Passed
Pull Request — master (#42)
by Théo
02:03
created

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