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

Configuration::retrieveOutputPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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