Passed
Pull Request — master (#77)
by Théo
03:56 queued 01:36
created

Configuration::retrieveShebang()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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