Completed
Pull Request — master (#37)
by Théo
31:28 queued 20:15
created

Configuration::retrieveGitVersionPlaceholder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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