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

Configuration::retrieveBlacklist()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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