Completed
Pull Request — master (#31)
by Théo
02:15
created

Configuration::retrieveDirectoryPaths()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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