Passed
Pull Request — master (#39)
by Théo
02:15
created

Configuration::getBasePathRetriever()   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 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
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 = self::retrieveAlias($raw);
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 retrieveAlias(stdClass $raw): string
515
    {
516
        $alias = $raw->alias ?? self::DEFAULT_ALIAS;
517
518
        Assertion::string($alias, 'Expected PHAR alias to be a string, got "%s" instead.');
519
520
        $alias = trim($alias);
521
522
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty.');
523
524
        return $alias;
525
    }
526
527
    private static function retrieveBasePath(string $file, stdClass $raw): string
528
    {
529
        if (isset($raw->{'base-path'})) {
530
            if (false === is_dir($raw->{'base-path'})) {
531
                throw new InvalidArgumentException(
532
                    sprintf(
533
                        'The base path "%s" is not a directory or does not exist.',
534
                        $raw->{'base-path'}
535
                    )
536
                );
537
            }
538
539
            return realpath($raw->{'base-path'});
540
        }
541
542
        return realpath(dirname($file));
543
    }
544
545
    /**
546
     * @return SplFileInfo[]
547
     */
548
    private static function retrieveBinaryDirectories(stdClass $raw, string $basePath): array
549
    {
550
        if (isset($raw->{'directories-bin'})) {
551
            $directories = (array) $raw->{'directories-bin'};
552
553
            array_walk(
554
                $directories,
555
                function (&$directory) use ($basePath): void {
556
                    $directory = $basePath
557
                        .DIRECTORY_SEPARATOR
558
                        .canonicalize($directory);
559
                }
560
            );
561
562
            return $directories;
563
        }
564
565
        return [];
566
    }
567
568
    /**
569
     * @param SplFileInfo[] $binaryDirectories
570
     * @param Closure       $blacklistFilter
571
     *
572
     * @return null|iterable|SplFileInfo[] the iterator
573
     */
574
    private static function retrieveBinaryDirectoriesIterator(array $binaryDirectories, Closure $blacklistFilter): ?iterable
575
    {
576
        if ([] !== $binaryDirectories) {
577
            return Finder::create()
578
                ->files()
579
                ->filter($blacklistFilter)
580
                ->ignoreVCS(true)
581
                ->in($binaryDirectories);
582
        }
583
584
        return null;
585
    }
586
587
    /**
588
     * @return SplFileInfo[] the list of paths
589
     */
590
    private static function retrieveBinaryFiles(stdClass $raw, string $basePath): array
591
    {
592
        if (isset($raw->{'files-bin'})) {
593
            $files = [];
594
595
            foreach ((array) $raw->{'files-bin'} as $file) {
596
                $files[] = new SplFileInfo(
597
                    $basePath.DIRECTORY_SEPARATOR.canonicalize($file)
598
                );
599
            }
600
601
            return $files;
602
        }
603
604
        return [];
605
    }
606
607
    /**
608
     * @return null|iterable|SplFileInfo[] the iterator
609
     */
610
    private static function retrieveBinaryFilesIterator(array $binaryFiles): ?iterable
611
    {
612
        if ([] !== $binaryFiles) {
613
            return new ArrayIterator($binaryFiles);
614
        }
615
616
        return null;
617
    }
618
619
    /**
620
     * @return string[]
621
     */
622
    private static function retrieveBlacklist(stdClass $raw): array
623
    {
624
        // TODO: only allow arrays if is set unlike the doc which says it can be a string
625
        if (isset($raw->blacklist)) {
626
            $blacklist = (array) $raw->blacklist;
627
628
            array_walk(
629
                $blacklist,
630
                function (&$file): void {
631
                    $file = canonicalize($file);
632
                }
633
            );
634
635
            return $blacklist;
636
        }
637
638
        return [];
639
    }
640
641
    /**
642
     * @param string   $basePath
643
     * @param string[] $blacklist
644
     *
645
     * @return Closure
646
     */
647
    private static function retrieveBlacklistFilter(string $basePath, array $blacklist): Closure
648
    {
649
        $base = sprintf(
650
            '/^%s/',
651
            preg_quote($basePath.DIRECTORY_SEPARATOR, '/')
652
        );
653
654
        return function (SplFileInfo $file) use ($base, $blacklist): ?bool {
655
            $path = canonicalize(
656
                preg_replace($base, '', $file->getPathname())
657
            );
658
659
            if (in_array($path, $blacklist, true)) {
660
                return false;
661
            }
662
663
            return null;
664
        };
665
    }
666
667
    /**
668
     * @param stdClass $raw
669
     * @param string   $basePath
670
     * @param Closure  $blacklistFilter
671
     *
672
     * @return iterable[]|SplFileInfo[][]
673
     */
674
    private static function retrieveBinaryIterators(stdClass $raw, string $basePath, Closure $blacklistFilter): array
675
    {
676
        if (isset($raw->{'finder-bin'})) {
677
            return self::processFinders($raw->{'finder-bin'}, $basePath, $blacklistFilter);
678
        }
679
680
        return [];
681
    }
682
683
    /**
684
     * @param stdClass $raw
685
     * @param string   $basePath
686
     *
687
     * @return string[]
688
     */
689
    private static function retrieveDirectories(stdClass $raw, string $basePath): array
690
    {
691
        // TODO: do not accept strings unlike the doc says and document that BC break
692
        // Need to do the same for bin, directories-bin, files, ~~directories~~
693
        if (isset($raw->directories)) {
694
            $directories = (array) $raw->directories;
695
696
            array_walk(
697
                $directories,
698
                function (&$directory) use ($basePath): void {
699
                    $directory = $basePath
700
                        .DIRECTORY_SEPARATOR
701
                        .rtrim(canonicalize($directory), DIRECTORY_SEPARATOR);
702
                }
703
            );
704
705
            return $directories;
706
        }
707
708
        return [];
709
    }
710
711
    /**
712
     * @param string[] $directories
713
     * @param Closure  $blacklistFilter
714
     *
715
     * @return null|iterable|SplFileInfo[]
716
     */
717
    private static function retrieveDirectoriesIterator(array $directories, Closure $blacklistFilter): ?iterable
718
    {
719
        if ([] !== $directories) {
720
            return Finder::create()
721
                ->files()
722
                ->filter($blacklistFilter)
723
                ->ignoreVCS(true)
724
                ->in($directories)
725
            ;
726
        }
727
728
        return null;
729
    }
730
731
    /**
732
     * @return SplFileInfo[]
733
     */
734
    private static function retrieveFiles(stdClass $raw, string $basePath): array
735
    {
736
        if (false === isset($raw->files)) {
737
            return [];
738
        }
739
740
        $files = [];
741
742
        foreach ((array) $raw->files as $file) {
743
            $file = new SplFileInfo(
744
                $path = $basePath.DIRECTORY_SEPARATOR.canonicalize($file)
745
            );
746
747
            if (false === $file->isFile()) {
748
                throw new InvalidArgumentException(
749
                    sprintf(
750
                        'The file "%s" does not exist or is not a file.',
751
                        $path
752
                    )
753
                );
754
            }
755
756
            $files[] = $file;
757
        }
758
759
        return $files;
760
    }
761
762
    /**
763
     * @param SplFileInfo[] $files
764
     *
765
     * @return null|iterable|SplFileInfo[]
766
     */
767
    private static function retrieveFilesIterator(array $files): ?iterable
768
    {
769
        if ([] !== $files) {
770
            return new ArrayIterator($files);
771
        }
772
773
        return null;
774
    }
775
776
    /**
777
     * @param stdClass $raw
778
     * @param string   $basePath
779
     * @param Closure  $blacklistFilter
780
     *
781
     * @return iterable[]|SplFileInfo[][]
782
     */
783
    private static function retrieveFilesIterators(stdClass $raw, string $basePath, Closure $blacklistFilter): array
784
    {
785
        // TODO: double check the way all files are included to make sure none is missing if one element is
786
        // misconfigured
787
        if (isset($raw->finder)) {
788
            return self::processFinders($raw->finder, $basePath, $blacklistFilter);
789
        }
790
791
        return [];
792
    }
793
794
    /**
795
     * @param array   $findersConfig   the configuration
796
     * @param string  $basePath
797
     * @param Closure $blacklistFilter
798
     *
799
     * @return Finder[]
800
     */
801
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
802
    {
803
        // TODO: add an example in the doc about the finder and a mention in `finder-bin` to look at finder
804
805
        $processFinderConfig = function ($methods) use ($basePath, $blacklistFilter): Finder {
806
            $finder = Finder::create()
807
                ->files()
808
                ->filter($blacklistFilter)
809
                ->ignoreVCS(true)
810
            ;
811
812
            if (isset($methods->in)) {
813
                $methods->in = (array) $methods->in;
814
815
                array_walk(
816
                    $methods->in,
817
                    function (&$directory) use ($basePath): void {
818
                        $directory = canonicalize(
819
                            $basePath.DIRECTORY_SEPARATOR.$directory
820
                        );
821
                    }
822
                );
823
            }
824
825
            foreach ($methods as $method => $arguments) {
826
                if (false === method_exists($finder, $method)) {
827
                    throw new InvalidArgumentException(
828
                        sprintf(
829
                            'The method "Finder::%s" does not exist.',
830
                            $method
831
                        )
832
                    );
833
                }
834
835
                $arguments = (array) $arguments;
836
837
                foreach ($arguments as $argument) {
838
                    $finder->$method($argument);
839
                }
840
            }
841
842
            return $finder;
843
        };
844
845
        return array_map($processFinderConfig, $findersConfig);
846
    }
847
848
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
849
    {
850
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
851
        // through that extension point so this is pretty much useless unless proven otherwise.
852
        if (false === isset($raw->bootstrap)) {
853
            return null;
854
        }
855
856
        $file = $raw->bootstrap;
857
858
        if (false === is_absolute($file)) {
859
            $file = canonicalize(
860
                $basePath.DIRECTORY_SEPARATOR.$file
861
            );
862
        }
863
864
        if (false === file_exists($file)) {
865
            throw new InvalidArgumentException(
866
                sprintf(
867
                    'The bootstrap path "%s" is not a file or does not exist.',
868
                    $file
869
                )
870
            );
871
        }
872
873
        return $file;
874
    }
875
876
    /**
877
     * @return Compactor[]
878
     */
879
    private static function retrieveCompactors(stdClass $raw): array
880
    {
881
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
882
        if (false === isset($raw->compactors)) {
883
            return [];
884
        }
885
886
        $compactors = [];
887
888
        foreach ((array) $raw->compactors as $class) {
889
            if (false === class_exists($class)) {
890
                throw new InvalidArgumentException(
891
                    sprintf(
892
                        'The compactor class "%s" does not exist.',
893
                        $class
894
                    )
895
                );
896
            }
897
898
            if (Php::class === $class || LegacyPhp::class === $class) {
899
                $compactor = self::createPhpCompactor($raw);
900
            } else {
901
                $compactor = new $class();
902
            }
903
904
            if (false === ($compactor instanceof Compactor)) {
905
                throw new InvalidArgumentException(
906
                    sprintf(
907
                        'The class "%s" is not a compactor class.',
908
                        $class
909
                    )
910
                );
911
            }
912
913
            $compactors[] = $compactor;
914
        }
915
916
        return $compactors;
917
    }
918
919
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
920
    {
921
        // TODO: if in dev mode (when added), do not comment about the compression.
922
        // If not, add a warning to notify the user if no compression algorithm is used
923
        // provided the PHAR is not configured for web purposes.
924
        // If configured for the web, add a warning when a compression algorithm is used
925
        // as this can result in an overhead. Add a doc link explaining this.
926
        //
927
        // Unlike the doc: do not accept integers and document this BC break.
928
        if (false === isset($raw->compression)) {
929
            return null;
930
        }
931
932
        if (false === is_string($raw->compression)) {
933
            Assertion::integer(
934
                $raw->compression,
935
                'Expected compression to be an algorithm name, found %s instead.'
936
            );
937
938
            return $raw->compression;
939
        }
940
941
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
942
943
        Assertion::inArray(
944
            $raw->compression,
945
            $knownAlgorithmNames,
946
            sprintf(
947
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
948
                implode('", "', $knownAlgorithmNames)
949
            )
950
        );
951
952
        $value = get_phar_compression_algorithms()[$raw->compression];
953
954
        // Phar::NONE is not valid for compressFiles()
955
        if (Phar::NONE === $value) {
956
            return null;
957
        }
958
959
        return $value;
960
    }
961
962
    private static function retrieveFileMode(stdClass $raw): ?int
963
    {
964
        if (isset($raw->chmod)) {
965
            return intval($raw->chmod, 8);
966
        }
967
968
        return null;
969
    }
970
971
    private static function retrieveMainScriptPath(stdClass $raw): ?string
972
    {
973
        // TODO: check if is used for the web as well when web is set to true
974
        // If that the case make this field mandatory otherwise adjust the check
975
        // rules accordinly to ensure we do not have an empty PHAR
976
        if (isset($raw->main)) {
977
            return canonicalize($raw->main);
978
        }
979
980
        return null;
981
    }
982
983
    private static function retrieveMainScriptContents(?string $mainScriptPath, string $basePath): ?string
984
    {
985
        if (null === $mainScriptPath) {
986
            return null;
987
        }
988
989
        $mainScriptPath = $basePath.DIRECTORY_SEPARATOR.$mainScriptPath;
990
991
        Assertion::readable($mainScriptPath);
992
993
        $contents = file_get_contents($mainScriptPath);
994
995
        // Remove the shebang line
996
        return preg_replace('/^#!.*\s*/', '', $contents);
997
    }
998
999
    /**
1000
     * @return string[][]
1001
     */
1002
    private static function retrieveMap(stdClass $raw): array
1003
    {
1004
        if (false === isset($raw->map)) {
1005
            return [];
1006
        }
1007
1008
        $map = [];
1009
1010
        foreach ((array) $raw->map as $item) {
1011
            $processed = [];
1012
1013
            foreach ($item as $match => $replace) {
1014
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1015
            }
1016
1017
            if (isset($processed['_empty_'])) {
1018
                $processed[''] = $processed['_empty_'];
1019
1020
                unset($processed['_empty_']);
1021
            }
1022
1023
            $map[] = $processed;
1024
        }
1025
1026
        return $map;
1027
    }
1028
1029
    /**
1030
     * @return mixed
1031
     */
1032
    private static function retrieveMetadata(stdClass $raw)
1033
    {
1034
        // TODO: the doc currently say this can be any value; check if true
1035
        // and if not add checks accordingly
1036
        //
1037
        // Also review the doc as I don't find it very helpful...
1038
        if (isset($raw->metadata)) {
1039
            if (is_object($raw->metadata)) {
1040
                return (array) $raw->metadata;
1041
            }
1042
1043
            return $raw->metadata;
1044
        }
1045
1046
        return null;
1047
    }
1048
1049
    private static function retrieveMimetypeMapping(stdClass $raw): array
1050
    {
1051
        // TODO: this parameter is not clear to me: review usage, doc & checks
1052
        if (isset($raw->mimetypes)) {
1053
            return (array) $raw->mimetypes;
1054
        }
1055
1056
        return [];
1057
    }
1058
1059
    private static function retrieveMungVariables(stdClass $raw): array
1060
    {
1061
        // TODO: this parameter is not clear to me: review usage, doc & checks
1062
        // TODO: add error/warning if used when web is not enabled
1063
        if (isset($raw->mung)) {
1064
            return (array) $raw->mung;
1065
        }
1066
1067
        return [];
1068
    }
1069
1070
    private static function retrieveNotFoundScriptPath(stdClass $raw): ?string
1071
    {
1072
        // TODO: this parameter is not clear to me: review usage, doc & checks
1073
        // TODO: add error/warning if used when web is not enabled
1074
        if (isset($raw->{'not-found'})) {
1075
            return $raw->{'not-found'};
1076
        }
1077
1078
        return null;
1079
    }
1080
1081
    private static function retrieveOutputPath(stdClass $raw, string $file): string
1082
    {
1083
        // TODO: make this path relative to the base path like everything else
1084
        // otherwise this is really confusing. This is a BC break that needs to be
1085
        // documented though (and update the doc accordingly as well)
1086
        $base = getcwd().DIRECTORY_SEPARATOR;
1087
1088
        if (isset($raw->output)) {
1089
            $path = $raw->output;
1090
1091
            if (false === is_absolute($path)) {
1092
                $path = canonicalize($base.$path);
1093
            }
1094
        } else {
1095
            $path = $base.self::DEFAULT_ALIAS;
1096
        }
1097
1098
        if (false !== strpos($path, '@'.'git-version@')) {
1099
            $gitVersion = self::retrieveGitVersion($file);
1100
1101
            $path = str_replace('@'.'git-version@', $gitVersion, $path);
1102
        }
1103
1104
        return $path;
1105
    }
1106
1107
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1108
    {
1109
        // TODO: add check to not allow this setting without the private key path
1110
        if (isset($raw->{'key-pass'})
1111
            && is_string($raw->{'key-pass'})
1112
        ) {
1113
            return $raw->{'key-pass'};
1114
        }
1115
1116
        return null;
1117
    }
1118
1119
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1120
    {
1121
        // TODO: If passed need to check its existence
1122
        // Also need
1123
1124
        if (isset($raw->key)) {
1125
            return $raw->key;
1126
        }
1127
1128
        return null;
1129
    }
1130
1131
    private static function retrieveReplacements(stdClass $raw): array
1132
    {
1133
        // TODO: add exmample in the doc
1134
        // Add checks against the values
1135
        if (isset($raw->replacements)) {
1136
            return (array) $raw->replacements;
1137
        }
1138
1139
        return [];
1140
    }
1141
1142
    private static function retrieveProcessedReplacements(
1143
        array $replacements,
1144
        stdClass $raw,
1145
        string $file
1146
    ): array {
1147
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1148
            $replacements[$git] = self::retrieveGitHash($file);
1149
        }
1150
1151
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1152
            $replacements[$git] = self::retrieveGitHash($file, true);
1153
        }
1154
1155
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1156
            $replacements[$git] = self::retrieveGitTag($file);
1157
        }
1158
1159
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1160
            $replacements[$git] = self::retrieveGitVersion($file);
1161
        }
1162
1163
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1164
            $replacements[$date] = self::retrieveDatetimeNow(
1165
                self::retrieveDatetimeFormat($raw)
1166
            );
1167
        }
1168
1169
        $sigil = self::retrieveReplacementSigil($raw);
1170
1171
        foreach ($replacements as $key => $value) {
1172
            unset($replacements[$key]);
1173
            $replacements["$sigil$key$sigil"] = $value;
1174
        }
1175
1176
        return $replacements;
1177
    }
1178
1179
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1180
    {
1181
        if (isset($raw->{'git-commit'})) {
1182
            return $raw->{'git-commit'};
1183
        }
1184
1185
        return null;
1186
    }
1187
1188
    /**
1189
     * @param string $file
1190
     * @param bool   $short Use the short version
1191
     *
1192
     * @return string the commit hash
1193
     */
1194
    private static function retrieveGitHash(string $file, bool $short = false): string
1195
    {
1196
        return self::runGitCommand(
1197
            sprintf(
1198
                'git log --pretty="%s" -n1 HEAD',
1199
                $short ? '%h' : '%H'
1200
            ),
1201
            $file
1202
        );
1203
    }
1204
1205
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1206
    {
1207
        if (isset($raw->{'git-commit-short'})) {
1208
            return $raw->{'git-commit-short'};
1209
        }
1210
1211
        return null;
1212
    }
1213
1214
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1215
    {
1216
        if (isset($raw->{'git-tag'})) {
1217
            return $raw->{'git-tag'};
1218
        }
1219
1220
        return null;
1221
    }
1222
1223
    private static function retrieveGitTag(string $file): ?string
1224
    {
1225
        return self::runGitCommand('git describe --tags HEAD', $file);
1226
    }
1227
1228
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1229
    {
1230
        if (isset($raw->{'git-version'})) {
1231
            return $raw->{'git-version'};
1232
        }
1233
1234
        return null;
1235
    }
1236
1237
    private static function retrieveGitVersion(string $file): ?string
1238
    {
1239
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1240
        // to avoid messing around with that
1241
1242
        try {
1243
            return self::retrieveGitTag($file);
1244
        } catch (RuntimeException $exception) {
1245
            try {
1246
                return self::retrieveGitHash($file, true);
1247
            } catch (RuntimeException $exception) {
1248
                throw new RuntimeException(
1249
                    sprintf(
1250
                        'The tag or commit hash could not be retrieved from "%s": %s',
1251
                        dirname($file),
1252
                        $exception->getMessage()
1253
                    ),
1254
                    0,
1255
                    $exception
1256
                );
1257
            }
1258
        }
1259
    }
1260
1261
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1262
    {
1263
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1264
        // Also make sure the documentation is up to date after.
1265
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1266
        // be better to have only one element IMO like:
1267
        //
1268
        // "datetime": {
1269
        //   "value": "val",
1270
        //   "format": "Y-m-d"
1271
        // }
1272
        //
1273
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1274
        // if the datetime format is the default one it's ok; but in any case the format should not
1275
        // be added without the datetime value...
1276
1277
        if (isset($raw->{'datetime'})) {
1278
            return $raw->{'datetime'};
1279
        }
1280
1281
        return null;
1282
    }
1283
1284
    private static function retrieveDatetimeNow(string $format)
1285
    {
1286
        $now = new DateTimeImmutable('now');
1287
1288
        $datetime = $now->format($format);
1289
1290
        if (!$datetime) {
1291
            throw new InvalidArgumentException(
1292
                sprintf(
1293
                    '""%s" is not a valid PHP date format',
1294
                    $format
1295
                )
1296
            );
1297
        }
1298
1299
        return $datetime;
1300
    }
1301
1302
    private static function retrieveDatetimeFormat(stdClass $raw): string
1303
    {
1304
        if (isset($raw->{'datetime_format'})) {
1305
            return $raw->{'datetime_format'};
1306
        }
1307
1308
        return self::DEFAULT_DATETIME_FORMAT;
1309
    }
1310
1311
    private static function retrieveReplacementSigil(stdClass $raw)
1312
    {
1313
        if (isset($raw->{'replacement-sigil'})) {
1314
            return $raw->{'replacement-sigil'};
1315
        }
1316
1317
        return self::DEFAULT_REPLACEMENT_SIGIL;
1318
    }
1319
1320
    private static function retrieveShebang(stdClass $raw): ?string
1321
    {
1322
        // TODO: unlike the doc says do not allow empty strings.
1323
        // Leverage `Assertion` here?
1324
        if (false === isset($raw->shebang)) {
1325
            return null;
1326
        }
1327
1328
        if (('' === $raw->shebang) || (false === $raw->shebang)) {
1329
            return '';
1330
        }
1331
1332
        $shebang = trim($raw->shebang);
1333
1334
        if ('#!' !== substr($shebang, 0, 2)) {
1335
            throw new InvalidArgumentException(
1336
                sprintf(
1337
                    'The shebang line must start with "#!": %s',
1338
                    $shebang
1339
                )
1340
            );
1341
        }
1342
1343
        return $shebang;
1344
    }
1345
1346
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1347
    {
1348
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1349
        // TODO: trigger a warning if the signing algorithm used is weak
1350
        // TODO: no longer accept strings & document BC break
1351
        if (false === isset($raw->algorithm)) {
1352
            return Phar::SHA1;
1353
        }
1354
1355
        if (is_string($raw->algorithm)) {
1356
            if (false === defined('Phar::'.$raw->algorithm)) {
1357
                throw new InvalidArgumentException(
1358
                    sprintf(
1359
                        'The signing algorithm "%s" is not supported.',
1360
                        $raw->algorithm
1361
                    )
1362
                );
1363
            }
1364
1365
            return constant('Phar::'.$raw->algorithm);
1366
        }
1367
1368
        return $raw->algorithm;
1369
    }
1370
1371
    private static function retrieveStubBanner(stdClass $raw): ?string
1372
    {
1373
        if (isset($raw->{'banner'})) {
1374
            return $raw->{'banner'};
1375
        }
1376
1377
        return null;
1378
    }
1379
1380
    private static function retrieveStubBannerPath(stdClass $raw): ?string
1381
    {
1382
        // TODO: if provided check its existence here or should it be defered to later?
1383
        // Works case this check can be duplicated...
1384
        //
1385
        // Check if is relative to base path: if not make it so (may be a BC break to document).
1386
        // Once checked, a mention in the doc that this path is relative to base-path (unless
1387
        // absolute).
1388
        // Check that the path is not provided if a banner is already provided.
1389
        if (isset($raw->{'banner-file'})) {
1390
            return canonicalize($raw->{'banner-file'});
1391
        }
1392
1393
        return null;
1394
    }
1395
1396
    private static function retrieveStubBannerFromFile(string $basePath, ?string $stubBannerPath): ?string
1397
    {
1398
        // TODO: Add checks
1399
        // TODO: The documentation is not clear enough IMO
1400
        if (null == $stubBannerPath) {
1401
            return null;
1402
        }
1403
1404
        $stubBannerPath = $basePath.DIRECTORY_SEPARATOR.$stubBannerPath;
1405
1406
        if (false === ($contents = @file_get_contents($stubBannerPath))) {
1407
            $errors = error_get_last();
1408
1409
            if (null === $errors) {
1410
                $errors = ['message' => 'failed to get contents of "'.$stubBannerPath.'""'];
1411
            }
1412
1413
            throw new InvalidArgumentException($errors['message']);
1414
        }
1415
1416
        return $contents;
1417
    }
1418
1419
    private static function retrieveStubPath(stdClass $raw): ?string
1420
    {
1421
        if (isset($raw->stub) && is_string($raw->stub)) {
1422
            return $raw->stub;
1423
        }
1424
1425
        return null;
1426
    }
1427
1428
    private static function retrieveIsExtractable(stdClass $raw): bool
1429
    {
1430
        // TODO: look it up, really not clear to me neither is the doc
1431
        if (isset($raw->extract)) {
1432
            return $raw->extract;
1433
        }
1434
1435
        return false;
1436
    }
1437
1438
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1439
    {
1440
        if (isset($raw->intercept)) {
1441
            return $raw->intercept;
1442
        }
1443
1444
        return false;
1445
    }
1446
1447
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1448
    {
1449
        if (isset($raw->{'key-pass'})
1450
            && (true === $raw->{'key-pass'})) {
1451
            return true;
1452
        }
1453
1454
        return false;
1455
    }
1456
1457
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1458
    {
1459
        if (isset($raw->stub) && (true === $raw->stub)) {
1460
            return true;
1461
        }
1462
1463
        return false;
1464
    }
1465
1466
    private static function retrieveIsWebPhar(stdClass $raw): bool
1467
    {
1468
        // TODO: doc is not clear enough
1469
        // Also check if is compatible web + CLI
1470
        if (isset($raw->web)) {
1471
            return $raw->web;
1472
        }
1473
1474
        return false;
1475
    }
1476
1477
    /**
1478
     * Runs a Git command on the repository.
1479
     *
1480
     * @param string $command the command
1481
     *
1482
     * @return string the trimmed output from the command
1483
     */
1484
    private static function runGitCommand(string $command, string $file): string
1485
    {
1486
        $path = dirname($file);
1487
1488
        $process = new Process($command, $path);
1489
1490
        if (0 === $process->run()) {
1491
            return trim($process->getOutput());
1492
        }
1493
1494
        throw new RuntimeException(
1495
            sprintf(
1496
                'The tag or commit hash could not be retrieved from "%s": %s',
1497
                $path,
1498
                $process->getErrorOutput()
1499
            )
1500
        );
1501
    }
1502
1503
    private static function createPhpCompactor(stdClass $raw): Compactor
1504
    {
1505
        // TODO: false === not set; check & add test/doc
1506
        $tokenizer = new Tokenizer();
1507
1508
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1509
            $tokenizer->ignore(
1510
                (array) $raw->annotations->ignore
1511
            );
1512
        }
1513
1514
        return new Php($tokenizer);
1515
    }
1516
}
1517