Passed
Pull Request — master (#76)
by Théo
02:23
created

Configuration::retrieveComposerLockFileContents()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
eloc 2
nc 2
nop 1
cc 2
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 function array_key_exists;
18
use function array_map;
19
use function array_merge;
20
use function array_unique;
21
use Assert\Assertion;
22
use Closure;
23
use DateTimeImmutable;
24
use function file_exists;
25
use function file_get_contents;
26
use Herrera\Annotations\Tokenizer;
27
use Herrera\Box\Compactor\Php as LegacyPhp;
28
use InvalidArgumentException;
29
use function is_link;
30
use KevinGH\Box\Compactor\Php;
31
use KevinGH\Box\Composer\ComposerConfiguration;
32
use Phar;
33
use RuntimeException;
34
use SplFileInfo;
35
use stdClass;
36
use Symfony\Component\Finder\Finder;
37
use Symfony\Component\Process\Process;
38
use function iter\chain;
39
use function KevinGH\Box\FileSystem\canonicalize;
40
use function KevinGH\Box\FileSystem\file_contents;
41
use function KevinGH\Box\FileSystem\make_path_absolute;
42
use function KevinGH\Box\FileSystem\make_path_relative;
43
44
final class Configuration
45
{
46
    private const DEFAULT_ALIAS = 'default.phar';
47
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
48
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
49
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
50
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
51
    private const DEFAULT_BANNER = <<<'BANNER'
52
Generated by Humbug Box.
53
54
@link https://github.com/humbug/box
55
BANNER;
56
    private const FILES_SETTINGS = [
57
        'files',
58
        'files-bin',
59
        'directories',
60
        'directories-bin',
61
        'finder',
62
        'finder-bin',
63
    ];
64
65
    private $fileMode;
66
    private $alias;
67
    private $basePathRetriever;
68
    private $files;
69
    private $binaryFiles;
70
    private $bootstrapFile;
71
    private $compactors;
72
    private $compressionAlgorithm;
73
    private $mainScriptPath;
74
    private $mainScriptContents;
75
    private $map;
76
    private $fileMapper;
77
    private $metadata;
78
    private $outputPath;
79
    private $privateKeyPassphrase;
80
    private $privateKeyPath;
81
    private $isPrivateKeyPrompt;
82
    private $processedReplacements;
83
    private $shebang;
84
    private $signingAlgorithm;
85
    private $stubBannerContents;
86
    private $stubBannerPath;
87
    private $stubPath;
88
    private $isInterceptFileFuncs;
89
    private $isStubGenerated;
90
91
    /**
92
     * @param null|string              $file
93
     * @param null|string              $alias
94
     * @param RetrieveRelativeBasePath $basePathRetriever     Utility to private the base path used and be able to retrieve a path relative to it (the base path)
95
     * @param SplFileInfo[]            $files                 List of files
96
     * @param SplFileInfo[]            $binaryFiles           List of binary files
97
     * @param null|string              $bootstrapFile         The bootstrap file path
98
     * @param Compactor[]              $compactors            List of file contents compactors
99
     * @param null|int                 $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
100
     * @param null|int                 $fileMode              File mode in octal form
101
     * @param string                   $mainScriptPath        The main script file path
102
     * @param string                   $mainScriptContents    The processed content of the main script file
103
     * @param MapFile                  $fileMapper            Utility to map the files from outside and inside the PHAR
104
     * @param mixed                    $metadata              The PHAR Metadata
105
     * @param string                   $outputPath
106
     * @param null|string              $privateKeyPassphrase
107
     * @param null|string              $privateKeyPath
108
     * @param bool                     $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
109
     * @param array                    $processedReplacements The processed list of replacement placeholders and their values
110
     * @param null|string              $shebang               The shebang line
111
     * @param int                      $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
112
     * @param null|string              $stubBannerContents    The stub banner comment
113
     * @param null|string              $stubBannerPath        The path to the stub banner comment file
114
     * @param null|string              $stubPath              The PHAR stub file path
115
     * @param bool                     $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
116
     * @param bool                     $isStubGenerated       Wether or not if the PHAR stub should be generated
117
     */
118
    private function __construct(
119
        ?string $file,
120
        ?string $alias,
121
        RetrieveRelativeBasePath $basePathRetriever,
122
        array $files,
123
        array $binaryFiles,
124
        ?string $bootstrapFile,
125
        array $compactors,
126
        ?int $compressionAlgorithm,
127
        ?int $fileMode,
128
        string $mainScriptPath,
129
        string $mainScriptContents,
130
        MapFile $fileMapper,
131
        $metadata,
132
        string $outputPath,
133
        ?string $privateKeyPassphrase,
134
        ?string $privateKeyPath,
135
        bool $isPrivateKeyPrompt,
136
        array $processedReplacements,
137
        ?string $shebang,
138
        int $signingAlgorithm,
139
        ?string $stubBannerContents,
140
        ?string $stubBannerPath,
141
        ?string $stubPath,
142
        bool $isInterceptFileFuncs,
143
        bool $isStubGenerated
144
    ) {
145
        Assertion::nullOrInArray(
146
            $compressionAlgorithm,
147
            get_phar_compression_algorithms(),
148
            sprintf(
149
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
150
                implode('", "', array_keys(get_phar_compression_algorithms()))
151
            )
152
        );
153
154
        $this->alias = $alias;
155
        $this->basePathRetriever = $basePathRetriever;
156
        $this->files = $files;
157
        $this->binaryFiles = $binaryFiles;
158
        $this->bootstrapFile = $bootstrapFile;
159
        $this->compactors = $compactors;
160
        $this->compressionAlgorithm = $compressionAlgorithm;
161
        $this->fileMode = $fileMode;
162
        $this->mainScriptPath = $mainScriptPath;
163
        $this->mainScriptContents = $mainScriptContents;
164
        $this->fileMapper = $fileMapper;
165
        $this->metadata = $metadata;
166
        $this->outputPath = $outputPath;
167
        $this->privateKeyPassphrase = $privateKeyPassphrase;
168
        $this->privateKeyPath = $privateKeyPath;
169
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
170
        $this->processedReplacements = $processedReplacements;
171
        $this->shebang = $shebang;
172
        $this->signingAlgorithm = $signingAlgorithm;
173
        $this->stubBannerContents = $stubBannerContents;
174
        $this->stubBannerPath = $stubBannerPath;
175
        $this->stubPath = $stubPath;
176
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
177
        $this->isStubGenerated = $isStubGenerated;
178
    }
179
180
    public static function create(?string $file, stdClass $raw): self
181
    {
182
        $alias = self::retrieveAlias($raw);
183
184
        $basePath = self::retrieveBasePath($file, $raw);
185
        $basePathRetriever = new RetrieveRelativeBasePath($basePath);
186
187
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath);
188
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
189
190
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath);
191
192
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
193
194
        if (self::shouldRetrieveAllFiles($file, $raw)) {
195
            $filesAggregate = self::retrieveAllFiles($basePath, $mainScriptPath, $blacklistFilter, $devPackages);
196
            $binaryFilesAggregate = [];
197
        } else {
198
            $files = self::retrieveFiles($raw, 'files', $basePath);
199
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
0 ignored issues
show
Bug introduced by
The call to KevinGH\Box\Configuration::retrieveDirectories() has too few arguments starting with devPackages. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

199
            /** @scrutinizer ignore-call */ 
200
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
200
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter);
201
202
            $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
203
204
            $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
205
            $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
206
            $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter);
207
208
            $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
209
        }
210
211
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
212
213
        $compactors = self::retrieveCompactors($raw);
214
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
215
216
        $fileMode = self::retrieveFileMode($raw);
217
218
        $map = self::retrieveMap($raw);
219
        $fileMapper = new MapFile($map);
220
221
        $metadata = self::retrieveMetadata($raw);
222
223
        $outputPath = self::retrieveOutputPath($raw, $basePath);
224
225
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
226
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
227
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
228
229
        $replacements = self::retrieveReplacements($raw);
230
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
231
232
        $shebang = self::retrieveShebang($raw);
233
234
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
235
236
        $stubBannerContents = self::retrieveStubBannerContents($raw);
237
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
238
239
        if (null !== $stubBannerPath) {
240
            $stubBannerContents = file_contents($stubBannerPath);
241
        }
242
243
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
244
245
        $stubPath = self::retrieveStubPath($raw, $basePath);
246
247
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
248
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
249
250
        return new self(
251
            $file,
252
            $alias,
253
            $basePathRetriever,
254
            $filesAggregate,
255
            $binaryFilesAggregate,
256
            $bootstrapFile,
257
            $compactors,
258
            $compressionAlgorithm,
259
            $fileMode,
260
            $mainScriptPath,
261
            $mainScriptContents,
262
            $fileMapper,
263
            $metadata,
264
            $outputPath,
265
            $privateKeyPassphrase,
266
            $privateKeyPath,
267
            $isPrivateKeyPrompt,
268
            $processedReplacements,
269
            $shebang,
270
            $signingAlgorithm,
271
            $stubBannerContents,
272
            $stubBannerPath,
273
            $stubPath,
274
            $isInterceptFileFuncs,
275
            $isStubGenerated
276
        );
277
    }
278
279
    public function getBasePathRetriever(): RetrieveRelativeBasePath
280
    {
281
        return $this->basePathRetriever;
282
    }
283
284
    public function getAlias(): ?string
285
    {
286
        return $this->alias;
287
    }
288
289
    public function getBasePath(): string
290
    {
291
        return $this->basePathRetriever->getBasePath();
292
    }
293
294
    /**
295
     * @return string[]
296
     */
297
    public function getFiles(): array
298
    {
299
        return $this->files;
300
    }
301
302
    /**
303
     * @return string[]
304
     */
305
    public function getBinaryFiles(): array
306
    {
307
        return $this->binaryFiles;
308
    }
309
310
    public function getBootstrapFile(): ?string
311
    {
312
        return $this->bootstrapFile;
313
    }
314
315
    public function loadBootstrap(): void
316
    {
317
        $file = $this->bootstrapFile;
318
319
        if (null !== $file) {
320
            include $file;
321
        }
322
    }
323
324
    /**
325
     * @return Compactor[] the list of compactors
326
     */
327
    public function getCompactors(): array
328
    {
329
        return $this->compactors;
330
    }
331
332
    public function getCompressionAlgorithm(): ?int
333
    {
334
        return $this->compressionAlgorithm;
335
    }
336
337
    public function getFileMode(): ?int
338
    {
339
        return $this->fileMode;
340
    }
341
342
    public function getMainScriptPath(): string
343
    {
344
        return $this->mainScriptPath;
345
    }
346
347
    public function getMainScriptContents(): string
348
    {
349
        return $this->mainScriptContents;
350
    }
351
352
    public function getOutputPath(): string
353
    {
354
        return $this->outputPath;
355
    }
356
357
    /**
358
     * @return string[]
359
     */
360
    public function getMap(): array
361
    {
362
        return $this->fileMapper->getMap();
363
    }
364
365
    public function getFileMapper(): MapFile
366
    {
367
        return $this->fileMapper;
368
    }
369
370
    /**
371
     * @return mixed
372
     */
373
    public function getMetadata()
374
    {
375
        return $this->metadata;
376
    }
377
378
    public function getPrivateKeyPassphrase(): ?string
379
    {
380
        return $this->privateKeyPassphrase;
381
    }
382
383
    public function getPrivateKeyPath(): ?string
384
    {
385
        return $this->privateKeyPath;
386
    }
387
388
    public function isPrivateKeyPrompt(): bool
389
    {
390
        return $this->isPrivateKeyPrompt;
391
    }
392
393
    public function getProcessedReplacements(): array
394
    {
395
        return $this->processedReplacements;
396
    }
397
398
    public function getShebang(): ?string
399
    {
400
        return $this->shebang;
401
    }
402
403
    public function getSigningAlgorithm(): int
404
    {
405
        return $this->signingAlgorithm;
406
    }
407
408
    public function getStubBannerContents(): ?string
409
    {
410
        return $this->stubBannerContents;
411
    }
412
413
    public function getStubBannerPath(): ?string
414
    {
415
        return $this->stubBannerPath;
416
    }
417
418
    public function getStubPath(): ?string
419
    {
420
        return $this->stubPath;
421
    }
422
423
    public function isInterceptFileFuncs(): bool
424
    {
425
        return $this->isInterceptFileFuncs;
426
    }
427
428
    public function isStubGenerated(): bool
429
    {
430
        return $this->isStubGenerated;
431
    }
432
433
    private static function retrieveAlias(stdClass $raw): ?string
434
    {
435
        if (false === isset($raw->alias)) {
436
            return null;
437
        }
438
439
        $alias = trim($raw->alias);
440
441
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
442
443
        return $alias;
444
    }
445
446
    private static function retrieveBasePath(?string $file, stdClass $raw): string
447
    {
448
        if (null === $file) {
449
            return getcwd();
450
        }
451
452
        if (false === isset($raw->{'base-path'})) {
453
            return realpath(dirname($file));
454
        }
455
456
        $basePath = trim($raw->{'base-path'});
457
458
        Assertion::directory(
459
            $basePath,
460
            'The base path "%s" is not a directory or does not exist.'
461
        );
462
463
        return realpath($basePath);
464
    }
465
466
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
467
    {
468
        if (null === $file) {
469
            return true;
470
        }
471
472
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
473
        $rawConfig = (array) $raw;
474
475
        foreach (self::FILES_SETTINGS as $key) {
476
            if (array_key_exists($key, $rawConfig)) {
477
                return false;
478
            }
479
        }
480
481
        return true;
482
    }
483
484
    /**
485
     * @param stdClass $raw
486
     * @param string   $basePath
487
     *
488
     * @return Closure
489
     */
490
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
491
    {
492
        $blacklist = self::retrieveBlacklist($raw, $basePath);
493
494
        return function (SplFileInfo $file) use ($blacklist): ?bool {
495
            if (in_array($file->getRealPath(), $blacklist, true)) {
496
                return false;
497
            }
498
499
            return null;
500
        };
501
    }
502
503
    /**
504
     * @param stdClass $raw
505
     * @param string $basePath
506
     *
507
     * @return string[]
508
     */
509
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
510
    {
511
        if (false === isset($raw->blacklist)) {
512
            return [];
513
        }
514
515
        $blacklist = $raw->blacklist;
516
517
        $normalizePath = function ($file) use ($basePath): string {
518
            return self::normalizeFilePath($file, $basePath);
519
        };
520
521
        return array_unique(array_map($normalizePath, $blacklist));
522
    }
523
524
    /**
525
     * @param stdClass $raw
526
     * @param string   $key      Config property name
527
     * @param string   $basePath
528
     *
529
     * @return SplFileInfo[]
530
     */
531
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
532
    {
533
        if (false === isset($raw->{$key})) {
534
            return [];
535
        }
536
537
        $files = (array) $raw->{$key};
538
539
        Assertion::allString($files);
540
541
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
542
            $file = self::normalizeFilePath($file, $basePath);
543
544
            if (is_link($file)) {
545
                // TODO: add this to baberlei/assert
546
                throw new InvalidArgumentException(
547
                    sprintf(
548
                        'Cannot add the link "%s": links are not supported.',
549
                        $file
550
                    )
551
                );
552
            }
553
554
            Assertion::file(
555
                $file,
556
                sprintf(
557
                    '"%s" must contain a list of existing files. Could not find "%%s".',
558
                    $key
559
                )
560
            );
561
562
            return new SplFileInfo($file);
563
        };
564
565
        return array_map($normalizePath, $files);
566
    }
567
568
    /**
569
     * @param stdClass $raw
570
     * @param string   $key             Config property name
571
     * @param string   $basePath
572
     * @param string[]   $devPackages
573
     * @param Closure  $blacklistFilter
574
     *
575
     * @return iterable|SplFileInfo[]
576
     */
577
    private static function retrieveDirectories(
578
        stdClass $raw,
579
        string $key,
580
        string $basePath,
581
        Closure $blacklistFilter,
582
        array $devPackages
583
    ): iterable
584
    {
585
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
586
587
        if ([] !== $directories) {
588
            return Finder::create()
0 ignored issues
show
Bug Best Practice introduced by
The expression return Symfony\Component...ages)->in($directories) returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
589
                ->files()
590
                ->filter($blacklistFilter)
591
                ->ignoreVCS(true)
592
                ->exclude($devPackages)
593
                ->in($directories)
594
            ;
595
        }
596
597
        return [];
598
    }
599
600
    /**
601
     * @param stdClass $raw
602
     * @param string   $basePath
603
     * @param Closure  $blacklistFilter
604
     *
605
     * @return iterable[]|SplFileInfo[][]
606
     */
607
    private static function retrieveFilesFromFinders(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): array
608
    {
609
        if (isset($raw->{$key})) {
610
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter);
611
        }
612
613
        return [];
614
    }
615
616
    /**
617
     * @param array   $findersConfig
618
     * @param string  $basePath
619
     * @param Closure $blacklistFilter
620
     *
621
     * @return Finder[]|SplFileInfo[][]
622
     */
623
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
624
    {
625
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter) {
626
            return self::processFinder($config, $basePath, $blacklistFilter);
0 ignored issues
show
Bug introduced by
The call to KevinGH\Box\Configuration::processFinder() has too few arguments starting with devPackages. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

626
            return self::/** @scrutinizer ignore-call */ processFinder($config, $basePath, $blacklistFilter);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
627
        };
628
629
        return array_map($processFinderConfig, $findersConfig);
630
    }
631
632
    /**
633
     * @param stdClass $config
634
     * @param string $basePath
635
     * @param Closure $blacklistFilter
636
     * @param string[] $devPackages
637
     *
638
     * @return Finder|SplFileInfo[]
639
     */
640
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter, array $devPackages): Finder
641
    {
642
        $finder = Finder::create()
643
            ->files()
644
            ->filter($blacklistFilter)
645
            ->exclude($devPackages)
646
            ->ignoreVCS(true)
647
        ;
648
649
        $normalizedConfig = (function (array $config, Finder $finder): array {
650
            $normalizedConfig = [];
651
652
            foreach ($config as $method => $arguments) {
653
                $method = trim($method);
654
                $arguments = (array) $arguments;
655
656
                Assertion::methodExists(
657
                    $method,
658
                    $finder,
659
                    'The method "Finder::%s" does not exist.'
660
                );
661
662
                $normalizedConfig[$method] = $arguments;
663
            }
664
665
            krsort($normalizedConfig);
666
667
            return $normalizedConfig;
668
        })((array) $config, $finder);
669
670
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
671
            $directory = self::normalizeDirectoryPath($directory, $basePath);
672
673
            if (is_link($directory)) {
674
                // TODO: add this to baberlei/assert
675
                throw new InvalidArgumentException(
676
                    sprintf(
677
                        'Cannot append the link "%s" to the Finder: links are not supported.',
678
                        $directory
679
                    )
680
                );
681
            }
682
683
            Assertion::directory($directory);
684
685
            return $directory;
686
        };
687
688
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
689
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
690
691
            if (is_link($fileOrDirectory)) {
692
                // TODO: add this to baberlei/assert
693
                throw new InvalidArgumentException(
694
                    sprintf(
695
                        'Cannot append the link "%s" to the Finder: links are not supported.',
696
                        $fileOrDirectory
697
                    )
698
                );
699
            }
700
701
            // TODO: add this to baberlei/assert
702
            if (false === file_exists($fileOrDirectory)) {
703
                throw new InvalidArgumentException(
704
                    sprintf(
705
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
706
                        $fileOrDirectory
707
                    )
708
                );
709
            }
710
711
            //TODO: add fileExists (as file or directory) to Assert
712
            if (false === is_file($fileOrDirectory)) {
713
                Assertion::directory($fileOrDirectory);
714
            } else {
715
                Assertion::file($fileOrDirectory);
716
            }
717
        };
718
719
        foreach ($normalizedConfig as $method => $arguments) {
720
            if ('in' === $method) {
721
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
722
            }
723
724
            if ('exclude' === $method) {
725
                $arguments = array_unique(array_map('trim', $arguments));
726
            }
727
728
            if ('append' === $method) {
729
                array_walk($arguments, $normalizeFileOrDirectory);
730
731
                $arguments = [$arguments];
732
            }
733
734
            foreach ($arguments as $argument) {
735
                $finder->$method($argument);
736
            }
737
        }
738
739
        return $finder;
740
    }
741
742
    /**
743
     * @param string $basePath
744
     * @param string $mainScriptPath
745
     * @param Closure $blacklistFilter
746
     * @param string[] $devPackages
747
     *
748
     * @return SplFileInfo[]
749
     */
750
    private static function retrieveAllFiles(
751
        string $basePath,
752
        string $mainScriptPath,
753
        Closure $blacklistFilter,
754
        array $devPackages
755
    ): array
756
    {
757
        $relativeDevPackages = array_map(
758
            function (string $packagePath) use ($basePath): string {
759
                return make_path_relative($packagePath, $basePath);
760
            },
761
            $devPackages
762
        );
763
764
        $finder = Finder::create()
765
            ->files()
766
            ->in($basePath)
767
            ->notPath(make_path_relative($mainScriptPath, $basePath))
768
            ->filter($blacklistFilter)
769
            ->exclude($relativeDevPackages)
770
            ->ignoreVCS(true)
771
        ;
772
773
        return array_filter(
774
            array_unique(
775
                array_map(
776
                    function (SplFileInfo $fileInfo): ?string {
777
                        if (is_link((string) $fileInfo)) {
778
                            return null;
779
                        }
780
781
                        return false !== $fileInfo->getRealPath() ? $fileInfo->getRealPath() : null;
782
                    },
783
                    iterator_to_array(
784
                        $finder
785
                    )
786
                )
787
            )
788
        );
789
    }
790
791
    /**
792
     * @param stdClass $raw
793
     * @param string   $key      Config property name
794
     * @param string   $basePath
795
     *
796
     * @return string[]
797
     */
798
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
799
    {
800
        if (false === isset($raw->{$key})) {
801
            return [];
802
        }
803
804
        $directories = $raw->{$key};
805
806
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
807
            $directory = self::normalizeDirectoryPath($directory, $basePath);
808
809
            if (is_link($directory)) {
810
                // TODO: add this to baberlei/assert
811
                throw new InvalidArgumentException(
812
                    sprintf(
813
                        'Cannot add the link "%s": links are not supported.',
814
                        $directory
815
                    )
816
                );
817
            }
818
819
            Assertion::directory(
820
                $directory,
821
                sprintf(
822
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
823
                    $key
824
                )
825
            );
826
827
            return $directory;
828
        };
829
830
        return array_map($normalizeDirectory, $directories);
831
    }
832
833
    private static function normalizeFilePath(string $file, string $basePath): string
834
    {
835
        return make_path_absolute(trim($file), $basePath);
836
    }
837
838
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
839
    {
840
        return make_path_absolute(trim($directory), $basePath);
841
    }
842
843
    private static function retrieveComposerLockFileContents(string $basePath): ?string
844
    {
845
        $composerLockFile = make_path_absolute('composer.lock', $basePath);
846
847
        return file_exists($composerLockFile) ? file_contents($composerLockFile) : null;
848
    }
849
850
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
851
    {
852
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
853
        // through that extension point so this is pretty much useless unless proven otherwise.
854
        if (false === isset($raw->bootstrap)) {
855
            return null;
856
        }
857
858
        $file = self::normalizeFilePath($raw->bootstrap, $basePath);
859
860
        Assertion::file($file, 'The bootstrap path "%s" is not a file or does not exist.');
861
862
        return $file;
863
    }
864
865
    /**
866
     * @return Compactor[]
867
     */
868
    private static function retrieveCompactors(stdClass $raw): array
869
    {
870
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
871
        if (false === isset($raw->compactors)) {
872
            return [];
873
        }
874
875
        $compactors = [];
876
877
        foreach ((array) $raw->compactors as $class) {
878
            Assertion::classExists($class, 'The compactor class "%s" does not exist.');
879
            Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
880
881
            if (Php::class === $class || LegacyPhp::class === $class) {
882
                $compactor = self::createPhpCompactor($raw);
883
            } else {
884
                $compactor = new $class();
885
            }
886
887
            $compactors[] = $compactor;
888
        }
889
890
        return $compactors;
891
    }
892
893
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
894
    {
895
        // TODO: if in dev mode (when added), do not comment about the compression.
896
        // If not, add a warning to notify the user if no compression algorithm is used
897
        // provided the PHAR is not configured for web purposes.
898
        // If configured for the web, add a warning when a compression algorithm is used
899
        // as this can result in an overhead. Add a doc link explaining this.
900
        //
901
        // Unlike the doc: do not accept integers and document this BC break.
902
        if (false === isset($raw->compression)) {
903
            return null;
904
        }
905
906
        if (false === is_string($raw->compression)) {
907
            Assertion::integer(
908
                $raw->compression,
909
                'Expected compression to be an algorithm name, found %s instead.'
910
            );
911
912
            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...
913
        }
914
915
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
916
917
        Assertion::inArray(
918
            $raw->compression,
919
            $knownAlgorithmNames,
920
            sprintf(
921
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
922
                implode('", "', $knownAlgorithmNames)
923
            )
924
        );
925
926
        $value = get_phar_compression_algorithms()[$raw->compression];
927
928
        // Phar::NONE is not valid for compressFiles()
929
        if (Phar::NONE === $value) {
930
            return null;
931
        }
932
933
        return $value;
934
    }
935
936
    private static function retrieveFileMode(stdClass $raw): ?int
937
    {
938
        if (isset($raw->chmod)) {
939
            return intval($raw->chmod, 8);
940
        }
941
942
        return null;
943
    }
944
945
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath): string
946
    {
947
        $main = isset($raw->main) ? $raw->main : self::DEFAULT_MAIN_SCRIPT;
948
949
        return self::normalizeFilePath($main, $basePath);
950
    }
951
952
    private static function retrieveMainScriptContents(string $mainScriptPath): string
953
    {
954
        $contents = file_contents($mainScriptPath);
955
956
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
957
        // PHAR entry point file.
958
        return preg_replace('/^#!.*\s*/', '', $contents);
959
    }
960
961
    /**
962
     * @return string[][]
963
     */
964
    private static function retrieveMap(stdClass $raw): array
965
    {
966
        if (false === isset($raw->map)) {
967
            return [];
968
        }
969
970
        $map = [];
971
972
        foreach ((array) $raw->map as $item) {
973
            $processed = [];
974
975
            foreach ($item as $match => $replace) {
976
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
977
            }
978
979
            if (isset($processed['_empty_'])) {
980
                $processed[''] = $processed['_empty_'];
981
982
                unset($processed['_empty_']);
983
            }
984
985
            $map[] = $processed;
986
        }
987
988
        return $map;
989
    }
990
991
    /**
992
     * @return mixed
993
     */
994
    private static function retrieveMetadata(stdClass $raw)
995
    {
996
        // TODO: the doc currently say this can be any value; check if true
997
        // and if not add checks accordingly
998
        //
999
        // Also review the doc as I don't find it very helpful...
1000
        if (isset($raw->metadata)) {
1001
            if (is_object($raw->metadata)) {
1002
                return (array) $raw->metadata;
1003
            }
1004
1005
            return $raw->metadata;
1006
        }
1007
1008
        return null;
1009
    }
1010
1011
    private static function retrieveOutputPath(stdClass $raw, string $basePath): string
1012
    {
1013
        if (isset($raw->output)) {
1014
            $path = $raw->output;
1015
        } else {
1016
            $path = self::DEFAULT_ALIAS;
1017
        }
1018
1019
        return make_path_absolute($path, $basePath);
1020
    }
1021
1022
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1023
    {
1024
        // TODO: add check to not allow this setting without the private key path
1025
        if (isset($raw->{'key-pass'})
1026
            && is_string($raw->{'key-pass'})
1027
        ) {
1028
            return $raw->{'key-pass'};
1029
        }
1030
1031
        return null;
1032
    }
1033
1034
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1035
    {
1036
        // TODO: If passed need to check its existence
1037
        // Also need
1038
1039
        if (isset($raw->key)) {
1040
            return $raw->key;
1041
        }
1042
1043
        return null;
1044
    }
1045
1046
    private static function retrieveReplacements(stdClass $raw): array
1047
    {
1048
        // TODO: add exmample in the doc
1049
        // Add checks against the values
1050
        if (isset($raw->replacements)) {
1051
            return (array) $raw->replacements;
1052
        }
1053
1054
        return [];
1055
    }
1056
1057
    private static function retrieveProcessedReplacements(
1058
        array $replacements,
1059
        stdClass $raw,
1060
        ?string $file
1061
    ): array {
1062
        if (null === $file) {
1063
            return [];
1064
        }
1065
1066
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1067
            $replacements[$git] = self::retrieveGitHash($file);
1068
        }
1069
1070
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1071
            $replacements[$git] = self::retrieveGitHash($file, true);
1072
        }
1073
1074
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1075
            $replacements[$git] = self::retrieveGitTag($file);
1076
        }
1077
1078
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1079
            $replacements[$git] = self::retrieveGitVersion($file);
1080
        }
1081
1082
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1083
            $replacements[$date] = self::retrieveDatetimeNow(
1084
                self::retrieveDatetimeFormat($raw)
1085
            );
1086
        }
1087
1088
        $sigil = self::retrieveReplacementSigil($raw);
1089
1090
        foreach ($replacements as $key => $value) {
1091
            unset($replacements[$key]);
1092
            $replacements["$sigil$key$sigil"] = $value;
1093
        }
1094
1095
        return $replacements;
1096
    }
1097
1098
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1099
    {
1100
        if (isset($raw->{'git-commit'})) {
1101
            return $raw->{'git-commit'};
1102
        }
1103
1104
        return null;
1105
    }
1106
1107
    /**
1108
     * @param string $file
1109
     * @param bool   $short Use the short version
1110
     *
1111
     * @return string the commit hash
1112
     */
1113
    private static function retrieveGitHash(string $file, bool $short = false): string
1114
    {
1115
        return self::runGitCommand(
1116
            sprintf(
1117
                'git log --pretty="%s" -n1 HEAD',
1118
                $short ? '%h' : '%H'
1119
            ),
1120
            $file
1121
        );
1122
    }
1123
1124
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1125
    {
1126
        if (isset($raw->{'git-commit-short'})) {
1127
            return $raw->{'git-commit-short'};
1128
        }
1129
1130
        return null;
1131
    }
1132
1133
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1134
    {
1135
        if (isset($raw->{'git-tag'})) {
1136
            return $raw->{'git-tag'};
1137
        }
1138
1139
        return null;
1140
    }
1141
1142
    private static function retrieveGitTag(string $file): ?string
1143
    {
1144
        return self::runGitCommand('git describe --tags HEAD', $file);
1145
    }
1146
1147
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1148
    {
1149
        if (isset($raw->{'git-version'})) {
1150
            return $raw->{'git-version'};
1151
        }
1152
1153
        return null;
1154
    }
1155
1156
    private static function retrieveGitVersion(string $file): ?string
1157
    {
1158
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1159
        // to avoid messing around with that
1160
1161
        try {
1162
            return self::retrieveGitTag($file);
1163
        } catch (RuntimeException $exception) {
1164
            try {
1165
                return self::retrieveGitHash($file, true);
1166
            } catch (RuntimeException $exception) {
1167
                throw new RuntimeException(
1168
                    sprintf(
1169
                        'The tag or commit hash could not be retrieved from "%s": %s',
1170
                        dirname($file),
1171
                        $exception->getMessage()
1172
                    ),
1173
                    0,
1174
                    $exception
1175
                );
1176
            }
1177
        }
1178
    }
1179
1180
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1181
    {
1182
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1183
        // Also make sure the documentation is up to date after.
1184
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1185
        // be better to have only one element IMO like:
1186
        //
1187
        // "datetime": {
1188
        //   "value": "val",
1189
        //   "format": "Y-m-d"
1190
        // }
1191
        //
1192
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1193
        // if the datetime format is the default one it's ok; but in any case the format should not
1194
        // be added without the datetime value...
1195
1196
        if (isset($raw->{'datetime'})) {
1197
            return $raw->{'datetime'};
1198
        }
1199
1200
        return null;
1201
    }
1202
1203
    private static function retrieveDatetimeNow(string $format)
1204
    {
1205
        $now = new DateTimeImmutable('now');
1206
1207
        $datetime = $now->format($format);
1208
1209
        if (!$datetime) {
1210
            throw new InvalidArgumentException(
1211
                sprintf(
1212
                    '""%s" is not a valid PHP date format',
1213
                    $format
1214
                )
1215
            );
1216
        }
1217
1218
        return $datetime;
1219
    }
1220
1221
    private static function retrieveDatetimeFormat(stdClass $raw): string
1222
    {
1223
        if (isset($raw->{'datetime_format'})) {
1224
            return $raw->{'datetime_format'};
1225
        }
1226
1227
        return self::DEFAULT_DATETIME_FORMAT;
1228
    }
1229
1230
    private static function retrieveReplacementSigil(stdClass $raw)
1231
    {
1232
        if (isset($raw->{'replacement-sigil'})) {
1233
            return $raw->{'replacement-sigil'};
1234
        }
1235
1236
        return self::DEFAULT_REPLACEMENT_SIGIL;
1237
    }
1238
1239
    private static function retrieveShebang(stdClass $raw): ?string
1240
    {
1241
        if (false === array_key_exists('shebang', (array) $raw)) {
1242
            return self::DEFAULT_SHEBANG;
1243
        }
1244
1245
        if (null === $raw->shebang) {
1246
            return null;
1247
        }
1248
1249
        $shebang = trim($raw->shebang);
1250
1251
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1252
        Assertion::true(
1253
            '#!' === substr($shebang, 0, 2),
1254
            sprintf(
1255
                'The shebang line must start with "#!". Got "%s" instead',
1256
                $shebang
1257
            )
1258
        );
1259
1260
        return $shebang;
1261
    }
1262
1263
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1264
    {
1265
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1266
        // TODO: trigger a warning if the signing algorithm used is weak
1267
        // TODO: no longer accept strings & document BC break
1268
        if (false === isset($raw->algorithm)) {
1269
            return Phar::SHA1;
1270
        }
1271
1272
        if (is_string($raw->algorithm)) {
1273
            if (false === defined('Phar::'.$raw->algorithm)) {
1274
                throw new InvalidArgumentException(
1275
                    sprintf(
1276
                        'The signing algorithm "%s" is not supported.',
1277
                        $raw->algorithm
1278
                    )
1279
                );
1280
            }
1281
1282
            return constant('Phar::'.$raw->algorithm);
1283
        }
1284
1285
        return $raw->algorithm;
1286
    }
1287
1288
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1289
    {
1290
        if (false === array_key_exists('banner', (array) $raw)) {
1291
            return self::DEFAULT_BANNER;
1292
        }
1293
1294
        if (null === $raw->banner) {
1295
            return null;
1296
        }
1297
1298
        $banner = $raw->banner;
1299
1300
        if (is_array($banner)) {
1301
            $banner = implode("\n", $banner);
1302
        }
1303
1304
        return $banner;
1305
    }
1306
1307
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1308
    {
1309
        if (false === isset($raw->{'banner-file'})) {
1310
            return null;
1311
        }
1312
1313
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1314
1315
        Assertion::file($bannerFile);
1316
1317
        return $bannerFile;
1318
    }
1319
1320
    private static function normalizeStubBannerContents(?string $contents): ?string
1321
    {
1322
        if (null === $contents) {
1323
            return null;
1324
        }
1325
1326
        $banner = explode("\n", $contents);
1327
        $banner = array_map('trim', $banner);
1328
1329
        return implode("\n", $banner);
1330
    }
1331
1332
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1333
    {
1334
        if (isset($raw->stub) && is_string($raw->stub)) {
1335
            $stubPath = make_path_absolute($raw->stub, $basePath);
1336
1337
            Assertion::file($stubPath);
1338
1339
            return $stubPath;
1340
        }
1341
1342
        return null;
1343
    }
1344
1345
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1346
    {
1347
        if (isset($raw->intercept)) {
1348
            return $raw->intercept;
1349
        }
1350
1351
        return false;
1352
    }
1353
1354
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1355
    {
1356
        if (isset($raw->{'key-pass'})
1357
            && (true === $raw->{'key-pass'})) {
1358
            return true;
1359
        }
1360
1361
        return false;
1362
    }
1363
1364
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1365
    {
1366
        if (isset($raw->stub) && (true === $raw->stub)) {
1367
            return true;
1368
        }
1369
1370
        return false;
1371
    }
1372
1373
    /**
1374
     * Runs a Git command on the repository.
1375
     *
1376
     * @param string $command the command
1377
     *
1378
     * @return string the trimmed output from the command
1379
     */
1380
    private static function runGitCommand(string $command, string $file): string
1381
    {
1382
        $path = dirname($file);
1383
1384
        $process = new Process($command, $path);
1385
1386
        if (0 === $process->run()) {
1387
            return trim($process->getOutput());
1388
        }
1389
1390
        throw new RuntimeException(
1391
            sprintf(
1392
                'The tag or commit hash could not be retrieved from "%s": %s',
1393
                $path,
1394
                $process->getErrorOutput()
1395
            )
1396
        );
1397
    }
1398
1399
    private static function createPhpCompactor(stdClass $raw): Compactor
1400
    {
1401
        // TODO: false === not set; check & add test/doc
1402
        $tokenizer = new Tokenizer();
1403
1404
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1405
            $tokenizer->ignore(
1406
                (array) $raw->annotations->ignore
1407
            );
1408
        }
1409
1410
        return new Php($tokenizer);
1411
    }
1412
}
1413