Passed
Push — master ( ddbdf0...1a517e )
by Théo
02:39
created

Configuration::retrieveMap()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.439
c 0
b 0
f 0
cc 5
eloc 12
nc 6
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use Herrera\Annotations\Tokenizer;
21
use Herrera\Box\Compactor\Php as LegacyPhp;
22
use InvalidArgumentException;
23
use KevinGH\Box\Compactor\Php;
24
use KevinGH\Box\Composer\ComposerConfiguration;
25
use Phar;
26
use RuntimeException;
27
use SplFileInfo;
28
use stdClass;
29
use Symfony\Component\Finder\Finder;
30
use Symfony\Component\Process\Process;
31
use function iter\chain;
32
use function KevinGH\Box\FileSystem\canonicalize;
33
use function KevinGH\Box\FileSystem\file_contents;
34
use function KevinGH\Box\FileSystem\longest_common_base_path;
35
use function KevinGH\Box\FileSystem\make_path_absolute;
36
use function KevinGH\Box\FileSystem\make_path_relative;
37
38
final class Configuration
39
{
40
    private const DEFAULT_ALIAS = 'default.phar';
41
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
42
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
43
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
44
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
45
    private const DEFAULT_BANNER = <<<'BANNER'
46
Generated by Humbug Box.
47
48
@link https://github.com/humbug/box
49
BANNER;
50
    private const FILES_SETTINGS = [
51
        'files',
52
        'files-bin',
53
        'directories',
54
        'directories-bin',
55
        'finder',
56
        'finder-bin',
57
    ];
58
59
    private $fileMode;
60
    private $alias;
61
    private $basePath;
62
    private $files;
63
    private $binaryFiles;
64
    private $bootstrapFile;
65
    private $compactors;
66
    private $compressionAlgorithm;
67
    private $mainScriptPath;
68
    private $mainScriptContents;
69
    private $map;
70
    private $fileMapper;
71
    private $metadata;
72
    private $outputPath;
73
    private $privateKeyPassphrase;
74
    private $privateKeyPath;
75
    private $isPrivateKeyPrompt;
76
    private $processedReplacements;
77
    private $shebang;
78
    private $signingAlgorithm;
79
    private $stubBannerContents;
80
    private $stubBannerPath;
81
    private $stubPath;
82
    private $isInterceptFileFuncs;
83
    private $isStubGenerated;
84
85
    /**
86
     * @param null|string   $file
87
     * @param null|string   $alias
88
     * @param string        $basePath              Utility to private the base path used and be able to retrieve a path relative to it (the base path)
89
     * @param SplFileInfo[] $files                 List of files
90
     * @param SplFileInfo[] $binaryFiles           List of binary files
91
     * @param null|string   $bootstrapFile         The bootstrap file path
92
     * @param Compactor[]   $compactors            List of file contents compactors
93
     * @param null|int      $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
94
     * @param null|int      $fileMode              File mode in octal form
95
     * @param string        $mainScriptPath        The main script file path
96
     * @param string        $mainScriptContents    The processed content of the main script file
97
     * @param MapFile       $fileMapper            Utility to map the files from outside and inside the PHAR
98
     * @param mixed         $metadata              The PHAR Metadata
99
     * @param string        $outputPath
100
     * @param null|string   $privateKeyPassphrase
101
     * @param null|string   $privateKeyPath
102
     * @param bool          $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
103
     * @param array         $processedReplacements The processed list of replacement placeholders and their values
104
     * @param null|string   $shebang               The shebang line
105
     * @param int           $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
106
     * @param null|string   $stubBannerContents    The stub banner comment
107
     * @param null|string   $stubBannerPath        The path to the stub banner comment file
108
     * @param null|string   $stubPath              The PHAR stub file path
109
     * @param bool          $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
110
     * @param bool          $isStubGenerated       Wether or not if the PHAR stub should be generated
111
     */
112
    private function __construct(
113
        ?string $file,
114
        ?string $alias,
115
        string $basePath,
116
        array $files,
117
        array $binaryFiles,
118
        ?string $bootstrapFile,
119
        array $compactors,
120
        ?int $compressionAlgorithm,
121
        ?int $fileMode,
122
        string $mainScriptPath,
123
        string $mainScriptContents,
124
        MapFile $fileMapper,
125
        $metadata,
126
        string $outputPath,
127
        ?string $privateKeyPassphrase,
128
        ?string $privateKeyPath,
129
        bool $isPrivateKeyPrompt,
130
        array $processedReplacements,
131
        ?string $shebang,
132
        int $signingAlgorithm,
133
        ?string $stubBannerContents,
134
        ?string $stubBannerPath,
135
        ?string $stubPath,
136
        bool $isInterceptFileFuncs,
137
        bool $isStubGenerated
138
    ) {
139
        Assertion::nullOrInArray(
140
            $compressionAlgorithm,
141
            get_phar_compression_algorithms(),
142
            sprintf(
143
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
144
                implode('", "', array_keys(get_phar_compression_algorithms()))
145
            )
146
        );
147
148
        $this->alias = $alias;
149
        $this->basePath = $basePath;
150
        $this->files = $files;
151
        $this->binaryFiles = $binaryFiles;
152
        $this->bootstrapFile = $bootstrapFile;
153
        $this->compactors = $compactors;
154
        $this->compressionAlgorithm = $compressionAlgorithm;
155
        $this->fileMode = $fileMode;
156
        $this->mainScriptPath = $mainScriptPath;
157
        $this->mainScriptContents = $mainScriptContents;
158
        $this->fileMapper = $fileMapper;
159
        $this->metadata = $metadata;
160
        $this->outputPath = $outputPath;
161
        $this->privateKeyPassphrase = $privateKeyPassphrase;
162
        $this->privateKeyPath = $privateKeyPath;
163
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
164
        $this->processedReplacements = $processedReplacements;
165
        $this->shebang = $shebang;
166
        $this->signingAlgorithm = $signingAlgorithm;
167
        $this->stubBannerContents = $stubBannerContents;
168
        $this->stubBannerPath = $stubBannerPath;
169
        $this->stubPath = $stubPath;
170
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
171
        $this->isStubGenerated = $isStubGenerated;
172
    }
173
174
    public static function create(?string $file, stdClass $raw): self
175
    {
176
        $alias = self::retrieveAlias($raw);
177
178
        $basePath = self::retrieveBasePath($file, $raw);
179
180
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath);
181
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
182
183
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath);
184
185
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
186
187
        if (self::shouldRetrieveAllFiles($file, $raw)) {
188
            $filesAggregate = self::retrieveAllFiles($basePath, $mainScriptPath, $blacklistFilter, $devPackages);
189
            $binaryFilesAggregate = [];
190
        } else {
191
            $files = self::retrieveFiles($raw, 'files', $basePath);
192
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
193
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
194
195
            $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
196
197
            $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
198
            $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
199
            $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
200
201
            $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
202
        }
203
204
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
205
206
        $compactors = self::retrieveCompactors($raw);
207
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
208
209
        $fileMode = self::retrieveFileMode($raw);
210
211
        $map = self::retrieveMap($raw);
212
        $fileMapper = new MapFile($map);
213
214
        $metadata = self::retrieveMetadata($raw);
215
216
        $outputPath = self::retrieveOutputPath($raw, $basePath);
217
218
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
219
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
220
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
221
222
        $replacements = self::retrieveReplacements($raw);
223
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
224
225
        $shebang = self::retrieveShebang($raw);
226
227
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
228
229
        $stubBannerContents = self::retrieveStubBannerContents($raw);
230
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
231
232
        if (null !== $stubBannerPath) {
233
            $stubBannerContents = file_contents($stubBannerPath);
234
        }
235
236
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
237
238
        $stubPath = self::retrieveStubPath($raw, $basePath);
239
240
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
241
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
242
243
        return new self(
244
            $file,
245
            $alias,
246
            $basePath,
247
            $filesAggregate,
248
            $binaryFilesAggregate,
249
            $bootstrapFile,
250
            $compactors,
251
            $compressionAlgorithm,
252
            $fileMode,
253
            $mainScriptPath,
254
            $mainScriptContents,
255
            $fileMapper,
256
            $metadata,
257
            $outputPath,
258
            $privateKeyPassphrase,
259
            $privateKeyPath,
260
            $isPrivateKeyPrompt,
261
            $processedReplacements,
262
            $shebang,
263
            $signingAlgorithm,
264
            $stubBannerContents,
265
            $stubBannerPath,
266
            $stubPath,
267
            $isInterceptFileFuncs,
268
            $isStubGenerated
269
        );
270
    }
271
272
    public function getAlias(): ?string
273
    {
274
        return $this->alias;
275
    }
276
277
    public function getBasePath(): string
278
    {
279
        return $this->basePath;
280
    }
281
282
    /**
283
     * @return string[]
284
     */
285
    public function getFiles(): array
286
    {
287
        return $this->files;
288
    }
289
290
    /**
291
     * @return string[]
292
     */
293
    public function getBinaryFiles(): array
294
    {
295
        return $this->binaryFiles;
296
    }
297
298
    public function getBootstrapFile(): ?string
299
    {
300
        return $this->bootstrapFile;
301
    }
302
303
    public function loadBootstrap(): void
304
    {
305
        $file = $this->bootstrapFile;
306
307
        if (null !== $file) {
308
            include $file;
309
        }
310
    }
311
312
    /**
313
     * @return Compactor[] the list of compactors
314
     */
315
    public function getCompactors(): array
316
    {
317
        return $this->compactors;
318
    }
319
320
    public function getCompressionAlgorithm(): ?int
321
    {
322
        return $this->compressionAlgorithm;
323
    }
324
325
    public function getFileMode(): ?int
326
    {
327
        return $this->fileMode;
328
    }
329
330
    public function getMainScriptPath(): string
331
    {
332
        return $this->mainScriptPath;
333
    }
334
335
    public function getMainScriptContents(): string
336
    {
337
        return $this->mainScriptContents;
338
    }
339
340
    public function getOutputPath(): string
341
    {
342
        return $this->outputPath;
343
    }
344
345
    /**
346
     * @return string[]
347
     */
348
    public function getMap(): array
349
    {
350
        return $this->fileMapper->getMap();
351
    }
352
353
    public function getFileMapper(): MapFile
354
    {
355
        return $this->fileMapper;
356
    }
357
358
    /**
359
     * @return mixed
360
     */
361
    public function getMetadata()
362
    {
363
        return $this->metadata;
364
    }
365
366
    public function getPrivateKeyPassphrase(): ?string
367
    {
368
        return $this->privateKeyPassphrase;
369
    }
370
371
    public function getPrivateKeyPath(): ?string
372
    {
373
        return $this->privateKeyPath;
374
    }
375
376
    public function isPrivateKeyPrompt(): bool
377
    {
378
        return $this->isPrivateKeyPrompt;
379
    }
380
381
    public function getProcessedReplacements(): array
382
    {
383
        return $this->processedReplacements;
384
    }
385
386
    public function getShebang(): ?string
387
    {
388
        return $this->shebang;
389
    }
390
391
    public function getSigningAlgorithm(): int
392
    {
393
        return $this->signingAlgorithm;
394
    }
395
396
    public function getStubBannerContents(): ?string
397
    {
398
        return $this->stubBannerContents;
399
    }
400
401
    public function getStubBannerPath(): ?string
402
    {
403
        return $this->stubBannerPath;
404
    }
405
406
    public function getStubPath(): ?string
407
    {
408
        return $this->stubPath;
409
    }
410
411
    public function isInterceptFileFuncs(): bool
412
    {
413
        return $this->isInterceptFileFuncs;
414
    }
415
416
    public function isStubGenerated(): bool
417
    {
418
        return $this->isStubGenerated;
419
    }
420
421
    private static function retrieveAlias(stdClass $raw): ?string
422
    {
423
        if (false === isset($raw->alias)) {
424
            return null;
425
        }
426
427
        $alias = trim($raw->alias);
428
429
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
430
431
        return $alias;
432
    }
433
434
    private static function retrieveBasePath(?string $file, stdClass $raw): string
435
    {
436
        if (null === $file) {
437
            return getcwd();
438
        }
439
440
        if (false === isset($raw->{'base-path'})) {
441
            return realpath(dirname($file));
442
        }
443
444
        $basePath = trim($raw->{'base-path'});
445
446
        Assertion::directory(
447
            $basePath,
448
            'The base path "%s" is not a directory or does not exist.'
449
        );
450
451
        return realpath($basePath);
452
    }
453
454
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
455
    {
456
        if (null === $file) {
457
            return true;
458
        }
459
460
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
461
        $rawConfig = (array) $raw;
462
463
        foreach (self::FILES_SETTINGS as $key) {
464
            if (array_key_exists($key, $rawConfig)) {
465
                return false;
466
            }
467
        }
468
469
        return true;
470
    }
471
472
    /**
473
     * @param stdClass $raw
474
     * @param string   $basePath
475
     *
476
     * @return Closure
477
     */
478
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
479
    {
480
        $blacklist = self::retrieveBlacklist($raw, $basePath);
481
482
        return function (SplFileInfo $file) use ($blacklist): ?bool {
483
            if (in_array($file->getRealPath(), $blacklist, true)) {
484
                return false;
485
            }
486
487
            return null;
488
        };
489
    }
490
491
    /**
492
     * @param stdClass $raw
493
     * @param string   $basePath
494
     *
495
     * @return string[]
496
     */
497
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
498
    {
499
        if (false === isset($raw->blacklist)) {
500
            return [];
501
        }
502
503
        $blacklist = $raw->blacklist;
504
505
        $normalizePath = function ($file) use ($basePath): string {
506
            return self::normalizeFilePath($file, $basePath);
507
        };
508
509
        return array_unique(array_map($normalizePath, $blacklist));
510
    }
511
512
    /**
513
     * @param stdClass $raw
514
     * @param string   $key      Config property name
515
     * @param string   $basePath
516
     *
517
     * @return SplFileInfo[]
518
     */
519
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
520
    {
521
        if (false === isset($raw->{$key})) {
522
            return [];
523
        }
524
525
        $files = (array) $raw->{$key};
526
527
        Assertion::allString($files);
528
529
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
530
            $file = self::normalizeFilePath($file, $basePath);
531
532
            if (is_link($file)) {
533
                // TODO: add this to baberlei/assert
534
                throw new InvalidArgumentException(
535
                    sprintf(
536
                        'Cannot add the link "%s": links are not supported.',
537
                        $file
538
                    )
539
                );
540
            }
541
542
            Assertion::file(
543
                $file,
544
                sprintf(
545
                    '"%s" must contain a list of existing files. Could not find "%%s".',
546
                    $key
547
                )
548
            );
549
550
            return new SplFileInfo($file);
551
        };
552
553
        return array_map($normalizePath, $files);
554
    }
555
556
    /**
557
     * @param stdClass $raw
558
     * @param string   $key             Config property name
559
     * @param string   $basePath
560
     * @param Closure  $blacklistFilter
561
     *
562
     * @return iterable|SplFileInfo[]
563
     */
564
    private static function retrieveDirectories(
565
        stdClass $raw,
566
        string $key,
567
        string $basePath,
568
        Closure $blacklistFilter
569
    ): iterable {
570
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
571
572
        if ([] !== $directories) {
573
            return Finder::create()
1 ignored issue
show
Bug Best Practice introduced by
The expression return Symfony\Component...true)->in($directories) returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
574
                ->files()
575
                ->filter($blacklistFilter)
576
                ->ignoreVCS(true)
577
                ->in($directories)
578
            ;
579
        }
580
581
        return [];
582
    }
583
584
    /**
585
     * @param stdClass $raw
586
     * @param string   $basePath
587
     * @param Closure  $blacklistFilter
588
     * @param string[] $devPackages
589
     *
590
     * @return iterable[]|SplFileInfo[][]
591
     */
592
    private static function retrieveFilesFromFinders(
593
        stdClass $raw,
594
        string $key,
595
        string $basePath,
596
        Closure $blacklistFilter,
597
        array $devPackages
598
    ): array {
599
        if (isset($raw->{$key})) {
600
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
601
        }
602
603
        return [];
604
    }
605
606
    /**
607
     * @param array    $findersConfig
608
     * @param string   $basePath
609
     * @param Closure  $blacklistFilter
610
     * @param string[] $devPackages
611
     *
612
     * @return Finder[]|SplFileInfo[][]
613
     */
614
    private static function processFinders(
615
        array $findersConfig,
616
        string $basePath,
617
        Closure $blacklistFilter,
618
        array $devPackages
619
    ): array {
620
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
621
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
622
        };
623
624
        return array_map($processFinderConfig, $findersConfig);
625
    }
626
627
    /**
628
     * @param stdClass $config
629
     * @param string   $basePath
630
     * @param Closure  $blacklistFilter
631
     * @param string[] $devPackages
632
     *
633
     * @return Finder|SplFileInfo[]
634
     */
635
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter, array $devPackages): Finder
636
    {
637
        $finder = Finder::create()
638
            ->files()
639
            ->filter($blacklistFilter)
640
            ->filter(
641
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
642
                    foreach ($devPackages as $devPackage) {
643
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
644
                            // File belongs to the dev package
645
                            return false;
646
                        }
647
                    }
648
649
                    return true;
650
                }
651
            )
652
            ->ignoreVCS(true)
653
        ;
654
655
        $normalizedConfig = (function (array $config, Finder $finder): array {
656
            $normalizedConfig = [];
657
658
            foreach ($config as $method => $arguments) {
659
                $method = trim($method);
660
                $arguments = (array) $arguments;
661
662
                Assertion::methodExists(
663
                    $method,
664
                    $finder,
665
                    'The method "Finder::%s" does not exist.'
666
                );
667
668
                $normalizedConfig[$method] = $arguments;
669
            }
670
671
            krsort($normalizedConfig);
672
673
            return $normalizedConfig;
674
        })((array) $config, $finder);
675
676
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
677
            $directory = self::normalizeDirectoryPath($directory, $basePath);
678
679
            if (is_link($directory)) {
680
                // TODO: add this to baberlei/assert
681
                throw new InvalidArgumentException(
682
                    sprintf(
683
                        'Cannot append the link "%s" to the Finder: links are not supported.',
684
                        $directory
685
                    )
686
                );
687
            }
688
689
            Assertion::directory($directory);
690
691
            return $directory;
692
        };
693
694
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
695
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
696
697
            if (is_link($fileOrDirectory)) {
698
                // TODO: add this to baberlei/assert
699
                throw new InvalidArgumentException(
700
                    sprintf(
701
                        'Cannot append the link "%s" to the Finder: links are not supported.',
702
                        $fileOrDirectory
703
                    )
704
                );
705
            }
706
707
            // TODO: add this to baberlei/assert
708
            if (false === file_exists($fileOrDirectory)) {
709
                throw new InvalidArgumentException(
710
                    sprintf(
711
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
712
                        $fileOrDirectory
713
                    )
714
                );
715
            }
716
717
            //TODO: add fileExists (as file or directory) to Assert
718
            if (false === is_file($fileOrDirectory)) {
719
                Assertion::directory($fileOrDirectory);
720
            } else {
721
                Assertion::file($fileOrDirectory);
722
            }
723
        };
724
725
        foreach ($normalizedConfig as $method => $arguments) {
726
            if ('in' === $method) {
727
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
728
            }
729
730
            if ('exclude' === $method) {
731
                $arguments = array_unique(array_map('trim', $arguments));
732
            }
733
734
            if ('append' === $method) {
735
                array_walk($arguments, $normalizeFileOrDirectory);
736
737
                $arguments = [$arguments];
738
            }
739
740
            foreach ($arguments as $argument) {
741
                $finder->$method($argument);
742
            }
743
        }
744
745
        return $finder;
746
    }
747
748
    /**
749
     * @param string   $basePath
750
     * @param string   $mainScriptPath
751
     * @param Closure  $blacklistFilter
752
     * @param string[] $devPackages
753
     *
754
     * @return SplFileInfo[]
755
     */
756
    private static function retrieveAllFiles(
757
        string $basePath,
758
        string $mainScriptPath,
759
        Closure $blacklistFilter,
760
        array $devPackages
761
    ): array {
762
        $relativeDevPackages = array_map(
763
            function (string $packagePath) use ($basePath): string {
764
                return make_path_relative($packagePath, $basePath);
765
            },
766
            $devPackages
767
        );
768
769
        $finder = Finder::create()
770
            ->files()
771
            ->in($basePath)
772
            ->notPath(make_path_relative($mainScriptPath, $basePath))
773
            ->filter($blacklistFilter)
774
            ->exclude($relativeDevPackages)
775
            ->ignoreVCS(true)
776
        ;
777
778
        return array_filter(
779
            array_unique(
780
                array_map(
781
                    function (SplFileInfo $fileInfo): ?string {
782
                        if (is_link((string) $fileInfo)) {
783
                            return null;
784
                        }
785
786
                        return false !== $fileInfo->getRealPath() ? $fileInfo->getRealPath() : null;
787
                    },
788
                    iterator_to_array(
789
                        $finder
790
                    )
791
                )
792
            )
793
        );
794
    }
795
796
    /**
797
     * @param stdClass $raw
798
     * @param string   $key      Config property name
799
     * @param string   $basePath
800
     *
801
     * @return string[]
802
     */
803
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
804
    {
805
        if (false === isset($raw->{$key})) {
806
            return [];
807
        }
808
809
        $directories = $raw->{$key};
810
811
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
812
            $directory = self::normalizeDirectoryPath($directory, $basePath);
813
814
            if (is_link($directory)) {
815
                // TODO: add this to baberlei/assert
816
                throw new InvalidArgumentException(
817
                    sprintf(
818
                        'Cannot add the link "%s": links are not supported.',
819
                        $directory
820
                    )
821
                );
822
            }
823
824
            Assertion::directory(
825
                $directory,
826
                sprintf(
827
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
828
                    $key
829
                )
830
            );
831
832
            return $directory;
833
        };
834
835
        return array_map($normalizeDirectory, $directories);
836
    }
837
838
    private static function normalizeFilePath(string $file, string $basePath): string
839
    {
840
        return make_path_absolute(trim($file), $basePath);
841
    }
842
843
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
844
    {
845
        return make_path_absolute(trim($directory), $basePath);
846
    }
847
848
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
849
    {
850
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
851
        // through that extension point so this is pretty much useless unless proven otherwise.
852
        if (false === isset($raw->bootstrap)) {
853
            return null;
854
        }
855
856
        $file = self::normalizeFilePath($raw->bootstrap, $basePath);
857
858
        Assertion::file($file, 'The bootstrap path "%s" is not a file or does not exist.');
859
860
        return $file;
861
    }
862
863
    /**
864
     * @return Compactor[]
865
     */
866
    private static function retrieveCompactors(stdClass $raw): array
867
    {
868
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
869
        if (false === isset($raw->compactors)) {
870
            return [];
871
        }
872
873
        $compactors = [];
874
875
        foreach ((array) $raw->compactors as $class) {
876
            Assertion::classExists($class, 'The compactor class "%s" does not exist.');
877
            Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
878
879
            if (Php::class === $class || LegacyPhp::class === $class) {
880
                $compactor = self::createPhpCompactor($raw);
881
            } else {
882
                $compactor = new $class();
883
            }
884
885
            $compactors[] = $compactor;
886
        }
887
888
        return $compactors;
889
    }
890
891
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
892
    {
893
        // TODO: if in dev mode (when added), do not comment about the compression.
894
        // If not, add a warning to notify the user if no compression algorithm is used
895
        // provided the PHAR is not configured for web purposes.
896
        // If configured for the web, add a warning when a compression algorithm is used
897
        // as this can result in an overhead. Add a doc link explaining this.
898
        //
899
        // Unlike the doc: do not accept integers and document this BC break.
900
        if (false === isset($raw->compression)) {
901
            return null;
902
        }
903
904
        if (false === is_string($raw->compression)) {
905
            Assertion::integer(
906
                $raw->compression,
907
                'Expected compression to be an algorithm name, found %s instead.'
908
            );
909
910
            return $raw->compression;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $raw->compression returns the type string which is incompatible with the type-hinted return null|integer.
Loading history...
911
        }
912
913
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
914
915
        Assertion::inArray(
916
            $raw->compression,
917
            $knownAlgorithmNames,
918
            sprintf(
919
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
920
                implode('", "', $knownAlgorithmNames)
921
            )
922
        );
923
924
        $value = get_phar_compression_algorithms()[$raw->compression];
925
926
        // Phar::NONE is not valid for compressFiles()
927
        if (Phar::NONE === $value) {
928
            return null;
929
        }
930
931
        return $value;
932
    }
933
934
    private static function retrieveFileMode(stdClass $raw): ?int
935
    {
936
        if (isset($raw->chmod)) {
937
            return intval($raw->chmod, 8);
938
        }
939
940
        return null;
941
    }
942
943
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath): string
944
    {
945
        $main = isset($raw->main) ? $raw->main : self::DEFAULT_MAIN_SCRIPT;
946
947
        return self::normalizeFilePath($main, $basePath);
948
    }
949
950
    private static function retrieveMainScriptContents(string $mainScriptPath): string
951
    {
952
        $contents = file_contents($mainScriptPath);
953
954
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
955
        // PHAR entry point file.
956
        return preg_replace('/^#!.*\s*/', '', $contents);
957
    }
958
959
    /**
960
     * @return string[][]
961
     */
962
    private static function retrieveMap(stdClass $raw): array
963
    {
964
        if (false === isset($raw->map)) {
965
            return [];
966
        }
967
968
        $map = [];
969
970
        foreach ((array) $raw->map as $item) {
971
            $processed = [];
972
973
            foreach ($item as $match => $replace) {
974
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
975
            }
976
977
            if (isset($processed['_empty_'])) {
978
                $processed[''] = $processed['_empty_'];
979
980
                unset($processed['_empty_']);
981
            }
982
983
            $map[] = $processed;
984
        }
985
986
        return $map;
987
    }
988
989
    /**
990
     * @return mixed
991
     */
992
    private static function retrieveMetadata(stdClass $raw)
993
    {
994
        // TODO: the doc currently say this can be any value; check if true
995
        // and if not add checks accordingly
996
        //
997
        // Also review the doc as I don't find it very helpful...
998
        if (isset($raw->metadata)) {
999
            if (is_object($raw->metadata)) {
1000
                return (array) $raw->metadata;
1001
            }
1002
1003
            return $raw->metadata;
1004
        }
1005
1006
        return null;
1007
    }
1008
1009
    private static function retrieveOutputPath(stdClass $raw, string $basePath): string
1010
    {
1011
        if (isset($raw->output)) {
1012
            $path = $raw->output;
1013
        } else {
1014
            $path = self::DEFAULT_ALIAS;
1015
        }
1016
1017
        return make_path_absolute($path, $basePath);
1018
    }
1019
1020
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1021
    {
1022
        // TODO: add check to not allow this setting without the private key path
1023
        if (isset($raw->{'key-pass'})
1024
            && is_string($raw->{'key-pass'})
1025
        ) {
1026
            return $raw->{'key-pass'};
1027
        }
1028
1029
        return null;
1030
    }
1031
1032
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1033
    {
1034
        // TODO: If passed need to check its existence
1035
        // Also need
1036
1037
        if (isset($raw->key)) {
1038
            return $raw->key;
1039
        }
1040
1041
        return null;
1042
    }
1043
1044
    private static function retrieveReplacements(stdClass $raw): array
1045
    {
1046
        // TODO: add exmample in the doc
1047
        // Add checks against the values
1048
        if (isset($raw->replacements)) {
1049
            return (array) $raw->replacements;
1050
        }
1051
1052
        return [];
1053
    }
1054
1055
    private static function retrieveProcessedReplacements(
1056
        array $replacements,
1057
        stdClass $raw,
1058
        ?string $file
1059
    ): array {
1060
        if (null === $file) {
1061
            return [];
1062
        }
1063
1064
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1065
            $replacements[$git] = self::retrieveGitHash($file);
1066
        }
1067
1068
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1069
            $replacements[$git] = self::retrieveGitHash($file, true);
1070
        }
1071
1072
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1073
            $replacements[$git] = self::retrieveGitTag($file);
1074
        }
1075
1076
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1077
            $replacements[$git] = self::retrieveGitVersion($file);
1078
        }
1079
1080
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1081
            $replacements[$date] = self::retrieveDatetimeNow(
1082
                self::retrieveDatetimeFormat($raw)
1083
            );
1084
        }
1085
1086
        $sigil = self::retrieveReplacementSigil($raw);
1087
1088
        foreach ($replacements as $key => $value) {
1089
            unset($replacements[$key]);
1090
            $replacements["$sigil$key$sigil"] = $value;
1091
        }
1092
1093
        return $replacements;
1094
    }
1095
1096
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1097
    {
1098
        if (isset($raw->{'git-commit'})) {
1099
            return $raw->{'git-commit'};
1100
        }
1101
1102
        return null;
1103
    }
1104
1105
    /**
1106
     * @param string $file
1107
     * @param bool   $short Use the short version
1108
     *
1109
     * @return string the commit hash
1110
     */
1111
    private static function retrieveGitHash(string $file, bool $short = false): string
1112
    {
1113
        return self::runGitCommand(
1114
            sprintf(
1115
                'git log --pretty="%s" -n1 HEAD',
1116
                $short ? '%h' : '%H'
1117
            ),
1118
            $file
1119
        );
1120
    }
1121
1122
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1123
    {
1124
        if (isset($raw->{'git-commit-short'})) {
1125
            return $raw->{'git-commit-short'};
1126
        }
1127
1128
        return null;
1129
    }
1130
1131
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1132
    {
1133
        if (isset($raw->{'git-tag'})) {
1134
            return $raw->{'git-tag'};
1135
        }
1136
1137
        return null;
1138
    }
1139
1140
    private static function retrieveGitTag(string $file): ?string
1141
    {
1142
        return self::runGitCommand('git describe --tags HEAD', $file);
1143
    }
1144
1145
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1146
    {
1147
        if (isset($raw->{'git-version'})) {
1148
            return $raw->{'git-version'};
1149
        }
1150
1151
        return null;
1152
    }
1153
1154
    private static function retrieveGitVersion(string $file): ?string
1155
    {
1156
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1157
        // to avoid messing around with that
1158
1159
        try {
1160
            return self::retrieveGitTag($file);
1161
        } catch (RuntimeException $exception) {
1162
            try {
1163
                return self::retrieveGitHash($file, true);
1164
            } catch (RuntimeException $exception) {
1165
                throw new RuntimeException(
1166
                    sprintf(
1167
                        'The tag or commit hash could not be retrieved from "%s": %s',
1168
                        dirname($file),
1169
                        $exception->getMessage()
1170
                    ),
1171
                    0,
1172
                    $exception
1173
                );
1174
            }
1175
        }
1176
    }
1177
1178
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1179
    {
1180
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1181
        // Also make sure the documentation is up to date after.
1182
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1183
        // be better to have only one element IMO like:
1184
        //
1185
        // "datetime": {
1186
        //   "value": "val",
1187
        //   "format": "Y-m-d"
1188
        // }
1189
        //
1190
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1191
        // if the datetime format is the default one it's ok; but in any case the format should not
1192
        // be added without the datetime value...
1193
1194
        if (isset($raw->{'datetime'})) {
1195
            return $raw->{'datetime'};
1196
        }
1197
1198
        return null;
1199
    }
1200
1201
    private static function retrieveDatetimeNow(string $format)
1202
    {
1203
        $now = new DateTimeImmutable('now');
1204
1205
        $datetime = $now->format($format);
1206
1207
        if (!$datetime) {
1208
            throw new InvalidArgumentException(
1209
                sprintf(
1210
                    '""%s" is not a valid PHP date format',
1211
                    $format
1212
                )
1213
            );
1214
        }
1215
1216
        return $datetime;
1217
    }
1218
1219
    private static function retrieveDatetimeFormat(stdClass $raw): string
1220
    {
1221
        if (isset($raw->{'datetime_format'})) {
1222
            return $raw->{'datetime_format'};
1223
        }
1224
1225
        return self::DEFAULT_DATETIME_FORMAT;
1226
    }
1227
1228
    private static function retrieveReplacementSigil(stdClass $raw)
1229
    {
1230
        if (isset($raw->{'replacement-sigil'})) {
1231
            return $raw->{'replacement-sigil'};
1232
        }
1233
1234
        return self::DEFAULT_REPLACEMENT_SIGIL;
1235
    }
1236
1237
    private static function retrieveShebang(stdClass $raw): ?string
1238
    {
1239
        if (false === array_key_exists('shebang', (array) $raw)) {
1240
            return self::DEFAULT_SHEBANG;
1241
        }
1242
1243
        if (null === $raw->shebang) {
1244
            return null;
1245
        }
1246
1247
        $shebang = trim($raw->shebang);
1248
1249
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1250
        Assertion::true(
1251
            '#!' === substr($shebang, 0, 2),
1252
            sprintf(
1253
                'The shebang line must start with "#!". Got "%s" instead',
1254
                $shebang
1255
            )
1256
        );
1257
1258
        return $shebang;
1259
    }
1260
1261
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1262
    {
1263
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1264
        // TODO: trigger a warning if the signing algorithm used is weak
1265
        // TODO: no longer accept strings & document BC break
1266
        if (false === isset($raw->algorithm)) {
1267
            return Phar::SHA1;
1268
        }
1269
1270
        if (is_string($raw->algorithm)) {
1271
            if (false === defined('Phar::'.$raw->algorithm)) {
1272
                throw new InvalidArgumentException(
1273
                    sprintf(
1274
                        'The signing algorithm "%s" is not supported.',
1275
                        $raw->algorithm
1276
                    )
1277
                );
1278
            }
1279
1280
            return constant('Phar::'.$raw->algorithm);
1281
        }
1282
1283
        return $raw->algorithm;
1284
    }
1285
1286
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1287
    {
1288
        if (false === array_key_exists('banner', (array) $raw)) {
1289
            return self::DEFAULT_BANNER;
1290
        }
1291
1292
        if (null === $raw->banner) {
1293
            return null;
1294
        }
1295
1296
        $banner = $raw->banner;
1297
1298
        if (is_array($banner)) {
1299
            $banner = implode("\n", $banner);
1300
        }
1301
1302
        return $banner;
1303
    }
1304
1305
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1306
    {
1307
        if (false === isset($raw->{'banner-file'})) {
1308
            return null;
1309
        }
1310
1311
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1312
1313
        Assertion::file($bannerFile);
1314
1315
        return $bannerFile;
1316
    }
1317
1318
    private static function normalizeStubBannerContents(?string $contents): ?string
1319
    {
1320
        if (null === $contents) {
1321
            return null;
1322
        }
1323
1324
        $banner = explode("\n", $contents);
1325
        $banner = array_map('trim', $banner);
1326
1327
        return implode("\n", $banner);
1328
    }
1329
1330
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1331
    {
1332
        if (isset($raw->stub) && is_string($raw->stub)) {
1333
            $stubPath = make_path_absolute($raw->stub, $basePath);
1334
1335
            Assertion::file($stubPath);
1336
1337
            return $stubPath;
1338
        }
1339
1340
        return null;
1341
    }
1342
1343
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1344
    {
1345
        if (isset($raw->intercept)) {
1346
            return $raw->intercept;
1347
        }
1348
1349
        return false;
1350
    }
1351
1352
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1353
    {
1354
        if (isset($raw->{'key-pass'})
1355
            && (true === $raw->{'key-pass'})) {
1356
            return true;
1357
        }
1358
1359
        return false;
1360
    }
1361
1362
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1363
    {
1364
        if (isset($raw->stub) && (true === $raw->stub)) {
1365
            return true;
1366
        }
1367
1368
        return false;
1369
    }
1370
1371
    /**
1372
     * Runs a Git command on the repository.
1373
     *
1374
     * @param string $command the command
1375
     *
1376
     * @return string the trimmed output from the command
1377
     */
1378
    private static function runGitCommand(string $command, string $file): string
1379
    {
1380
        $path = dirname($file);
1381
1382
        $process = new Process($command, $path);
1383
1384
        if (0 === $process->run()) {
1385
            return trim($process->getOutput());
1386
        }
1387
1388
        throw new RuntimeException(
1389
            sprintf(
1390
                'The tag or commit hash could not be retrieved from "%s": %s',
1391
                $path,
1392
                $process->getErrorOutput()
1393
            )
1394
        );
1395
    }
1396
1397
    private static function createPhpCompactor(stdClass $raw): Compactor
1398
    {
1399
        // TODO: false === not set; check & add test/doc
1400
        $tokenizer = new Tokenizer();
1401
1402
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1403
            $tokenizer->ignore(
1404
                (array) $raw->annotations->ignore
1405
            );
1406
        }
1407
1408
        return new Php($tokenizer);
1409
    }
1410
}
1411