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

Configuration::shouldRetrieveAllFiles()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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