Passed
Push — master ( 00b67f...9b5713 )
by Théo
02:00
created

Configuration::getMainScriptContents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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