Passed
Push — master ( 812a1d...639186 )
by Théo
02:22
created

Configuration::getCompactors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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