Passed
Push — master ( a6e730...dc0261 )
by Théo
02:01
created

Configuration::getStubPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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