Passed
Push — master ( b6bb22...3224b2 )
by Théo
02:20
created

Configuration::retrieveShebang()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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