Passed
Push — master ( 22acbe...a6e730 )
by Théo
02:08
created

Configuration::retrieveStubBannerPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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