Passed
Pull Request — master (#77)
by Théo
02:35
created

Configuration::getBasePathRetriever()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
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;
0 ignored issues
show
Bug introduced by
The type KevinGH\Box\Composer\ComposerConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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