PharInfo   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 44
eloc 118
dl 0
loc 307
rs 8.8798
c 2
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getStubContent() 0 3 1
A getCompression() 0 3 1
A getFileMeta() 0 14 2
A dumpPhar() 0 18 2
A getTmp() 0 3 1
A getSignature() 0 3 1
A initStubFileName() 0 4 2
A getFileName() 0 3 1
A getFilesCompressionCount() 0 7 2
A equals() 0 6 3
A getTimestamp() 0 3 1
A getFiles() 0 3 1
A calculateCompressionCount() 0 12 2
A getPubKeyContent() 0 3 1
A getNormalizedMetadata() 0 3 1
A getFile() 0 3 1
A initAlgorithms() 0 7 3
A __destruct() 0 13 3
A getVersion() 0 4 1
A hasPubKey() 0 3 1
A __construct() 0 25 3
A getRequirements() 0 29 4
A getStubPath() 0 3 1
A contentEquals() 0 18 4
A loadDumpedPharFiles() 0 17 1

How to fix   Complexity   

Complex Class

Complex classes like PharInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PharInfo, and based on these observations, apply Extract Interface, too.

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
/*
16
 * This file originates from https://github.com/paragonie/pharaoh.
17
 *
18
 * For maintenance reasons it had to be in-lined within Box. To simplify the
19
 * configuration for PHP-CS-Fixer, the original license is in-lined as follows:
20
 *
21
 * The MIT License (MIT)
22
 *
23
 * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises
24
 *
25
 * Permission is hereby granted, free of charge, to any person obtaining a copy
26
 * of this software and associated documentation files (the "Software"), to deal
27
 * in the Software without restriction, including without limitation the rights
28
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29
 * copies of the Software, and to permit persons to whom the Software is
30
 * furnished to do so, subject to the following conditions:
31
 *
32
 * The above copyright notice and this permission notice shall be included in all
33
 * copies or substantial portions of the Software.
34
 *
35
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41
 * SOFTWARE.
42
 */
43
44
namespace KevinGH\Box\Phar;
45
46
use Fidry\FileSystem\FS;
47
use KevinGH\Box\Console\Command\Extract;
48
use KevinGH\Box\ExecutableFinder;
49
use KevinGH\Box\Phar\Throwable\InvalidPhar;
50
use KevinGH\Box\RequirementChecker\Requirement;
51
use KevinGH\Box\RequirementChecker\Requirements;
52
use KevinGH\Box\RequirementChecker\Throwable\InvalidRequirements;
53
use KevinGH\Box\RequirementChecker\Throwable\NoRequirementsFound;
54
use OutOfBoundsException;
55
use Phar;
56
use Symfony\Component\Filesystem\Path;
57
use Symfony\Component\Finder\Finder;
58
use Symfony\Component\Finder\SplFileInfo;
59
use Symfony\Component\Process\Exception\ProcessFailedException;
60
use Symfony\Component\Process\Process;
61
use Throwable;
62
use function array_key_exists;
63
use function array_map;
64
use function bin2hex;
65
use function file_exists;
66
use function is_readable;
67
use function iter\mapKeys;
68
use function iter\toArrayWithKeys;
69
use function random_bytes;
70
use function sprintf;
71
use const DIRECTORY_SEPARATOR;
72
73
/**
74
 * @private
75
 *
76
 * PharInfo is a wrapper around the native Phar class. Its goal is to provide an equivalent API whilst being in-memory
77
 * safe.
78
 *
79
 * Indeed, the native Phar API is extremely limited due to the fact that it loads the code in-memory. This pollutes the
80
 * current process and will result in a crash if another PHAR with the same alias is loaded. This PharInfo class
81
 * circumvents those issues by extracting all the desired information in a separate process.
82
 */
83
final class PharInfo
84
{
85
    public const BOX_REQUIREMENTS = '.box/.requirements.php';
86
87
    private static array $ALGORITHMS;
88
    private static string $stubfile;
89
90
    private readonly PharMeta $meta;
0 ignored issues
show
Bug introduced by
The type KevinGH\Box\Phar\PharMeta was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
91
    private readonly string $tmp;
92
    private readonly string $file;
93
    private readonly string $fileName;
94
    private array $compressionCount;
95
96
    /**
97
     * @var array<string, SplFileInfo>
98
     */
99
    private readonly array $files;
100
101
    public function __construct(string $file)
102
    {
103
        $file = Path::canonicalize($file);
104
105
        if (!file_exists($file)) {
106
            throw InvalidPhar::fileNotFound($file);
107
        }
108
109
        if (!is_readable($file)) {
110
            throw InvalidPhar::fileNotReadable($file);
111
        }
112
113
        self::initAlgorithms();
114
        self::initStubFileName();
115
116
        $this->file = $file;
0 ignored issues
show
Bug introduced by
The property file is declared read-only in KevinGH\Box\Phar\PharInfo.
Loading history...
117
        $this->fileName = basename($file);
0 ignored issues
show
Bug introduced by
The property fileName is declared read-only in KevinGH\Box\Phar\PharInfo.
Loading history...
118
119
        $this->tmp = FS::makeTmpDir('HumbugBox', 'Pharaoh');
0 ignored issues
show
Bug introduced by
The property tmp is declared read-only in KevinGH\Box\Phar\PharInfo.
Loading history...
120
121
        self::dumpPhar($file, $this->tmp);
122
        [
123
            $this->meta,
0 ignored issues
show
Bug introduced by
The property meta is declared read-only in KevinGH\Box\Phar\PharInfo.
Loading history...
124
            $this->files,
0 ignored issues
show
Bug introduced by
The property files is declared read-only in KevinGH\Box\Phar\PharInfo.
Loading history...
125
        ] = self::loadDumpedPharFiles($this->tmp);
126
    }
127
128
    public function __destruct()
129
    {
130
        unset($this->pharInfo);
0 ignored issues
show
Bug Best Practice introduced by
The property pharInfo does not exist on KevinGH\Box\Phar\PharInfo. Did you maybe forget to declare it?
Loading history...
131
132
        if (isset($this->phar)) {
133
            $path = $this->phar->getPath();
134
            unset($this->phar);
135
136
            Phar::unlinkArchive($path);
137
        }
138
139
        if (isset($this->tmp)) {
140
            FS::remove($this->tmp);
141
        }
142
    }
143
144
    public function getTmp(): string
145
    {
146
        return $this->tmp;
147
    }
148
149
    public function getFile(): string
150
    {
151
        return $this->file;
152
    }
153
154
    public function getPubKeyContent(): ?string
155
    {
156
        return $this->meta->pubKeyContent;
157
    }
158
159
    public function hasPubKey(): bool
160
    {
161
        return null !== $this->getPubKeyContent();
162
    }
163
164
    public function getFileName(): string
165
    {
166
        return $this->fileName;
167
    }
168
169
    public function equals(self $pharInfo): bool
170
    {
171
        return
172
            $this->contentEquals($pharInfo)
173
            && $this->getCompression() === $pharInfo->getCompression()
174
            && $this->getNormalizedMetadata() === $pharInfo->getNormalizedMetadata();
175
    }
176
177
    /**
178
     * Checks if the content of the given PHAR equals the current one. Note that by content is meant
179
     * the list of files and their content. The files compression or the PHAR metadata are not considered.
180
     */
181
    private function contentEquals(self $pharInfo): bool
182
    {
183
        // The signature only checks if the contents are equal (same files, each files same content), but do
184
        // not check the compression of the files.
185
        // As a result, we also need to check the compression of each file.
186
        if ($this->getSignature() != $pharInfo->getSignature()) {
187
            return false;
188
        }
189
190
        foreach ($this->meta->filesMeta as $file => ['compression' => $compressionAlgorithm]) {
191
            ['compression' => $otherCompressionAlgorithm] = $this->getFileMeta($file);
192
193
            if ($otherCompressionAlgorithm !== $compressionAlgorithm) {
194
                return false;
195
            }
196
        }
197
198
        return true;
199
    }
200
201
    public function getCompression(): CompressionAlgorithm
202
    {
203
        return $this->meta->compression;
204
    }
205
206
    /**
207
     * @return array<string, positive-int|0> The number of files per compression algorithm label.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, positive-int|0> at position 4 could not be parsed: Unknown type name 'positive-int' at position 4 in array<string, positive-int|0>.
Loading history...
208
     */
209
    public function getFilesCompressionCount(): array
210
    {
211
        if (!isset($this->compressionCount)) {
212
            $this->compressionCount = self::calculateCompressionCount($this->meta->filesMeta);
213
        }
214
215
        return $this->compressionCount;
216
    }
217
218
    /**
219
     * @return array{'compression': CompressionAlgorithm, compressedSize: int}
220
     */
221
    public function getFileMeta(string $path): array
222
    {
223
        $meta = $this->meta->filesMeta[$path] ?? null;
224
225
        if (null === $meta) {
226
            throw new OutOfBoundsException(
227
                sprintf(
228
                    'No metadata found for the file "%s".',
229
                    $path,
230
                ),
231
            );
232
        }
233
234
        return $meta;
235
    }
236
237
    public function getVersion(): ?string
238
    {
239
        // TODO: review this fallback value
240
        return $this->meta->version ?? 'No information found';
241
    }
242
243
    public function getNormalizedMetadata(): ?string
244
    {
245
        return $this->meta->normalizedMetadata;
246
    }
247
248
    public function getTimestamp(): int
249
    {
250
        return $this->meta->timestamp;
251
    }
252
253
    public function getSignature(): ?array
254
    {
255
        return $this->meta->signature;
256
    }
257
258
    public function getStubPath(): string
259
    {
260
        return Extract::STUB_PATH;
261
    }
262
263
    public function getStubContent(): ?string
264
    {
265
        return $this->meta->stub;
266
    }
267
268
    /**
269
     * @return array<string, SplFileInfo>
270
     */
271
    public function getFiles(): array
272
    {
273
        return $this->files;
274
    }
275
276
    /**
277
     * @throws NoRequirementsFound
278
     * @throws InvalidRequirements
279
     */
280
    public function getRequirements(): Requirements
281
    {
282
        $file = $this->getFileName();
283
284
        if (!array_key_exists(self::BOX_REQUIREMENTS, $this->files)) {
285
            throw NoRequirementsFound::forFile($file);
286
        }
287
288
        $evaluatedRequirements = require $this->files[self::BOX_REQUIREMENTS]->getPathname();
289
290
        if (!is_array($evaluatedRequirements)) {
291
            throw InvalidRequirements::forRequirements($file, $evaluatedRequirements);
292
        }
293
294
        try {
295
            return new Requirements(
296
                array_map(
297
                    Requirement::fromArray(...),
298
                    $evaluatedRequirements,
299
                ),
300
            );
301
        } catch (Throwable $throwable) {
302
            throw new InvalidRequirements(
303
                sprintf(
304
                    'Could not interpret Box\'s RequirementChecker shipped in "%s": %s',
305
                    $file,
306
                    $throwable->getMessage(),
307
                ),
308
                previous: $throwable,
309
            );
310
        }
311
    }
312
313
    private static function initAlgorithms(): void
314
    {
315
        if (!isset(self::$ALGORITHMS)) {
316
            self::$ALGORITHMS = [];
317
318
            foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) {
319
                self::$ALGORITHMS[$compressionAlgorithm->value] = $compressionAlgorithm->name;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on KevinGH\Box\Phar\CompressionAlgorithm.
Loading history...
320
            }
321
        }
322
    }
323
324
    private static function initStubFileName(): void
325
    {
326
        if (!isset(self::$stubfile)) {
327
            self::$stubfile = bin2hex(random_bytes(12)).'.pharstub';
328
        }
329
    }
330
331
    private static function dumpPhar(string $file, string $tmp): void
332
    {
333
        $extractPharProcess = new Process([
334
            ExecutableFinder::findPhpExecutable(),
335
            ExecutableFinder::findBoxExecutable(),
336
            'extract',
337
            $file,
338
            $tmp,
339
            '--no-interaction',
340
            '--internal',
341
        ]);
342
        $extractPharProcess->run();
343
344
        if (false === $extractPharProcess->isSuccessful()) {
345
            throw new InvalidPhar(
346
                $extractPharProcess->getErrorOutput(),
347
                $extractPharProcess->getExitCode(),
348
                new ProcessFailedException($extractPharProcess),
349
            );
350
        }
351
    }
352
353
    /**
354
     * @return array{PharMeta, array<string, SplFileInfo>}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{PharMeta, array<string, SplFileInfo>} at position 2 could not be parsed: Expected ':' at position 2, but found 'PharMeta'.
Loading history...
355
     */
356
    private static function loadDumpedPharFiles(string $tmp): array
357
    {
358
        $dumpedFiles = toArrayWithKeys(
359
            mapKeys(
360
                static fn (string $filePath) => Path::makeRelative($filePath, $tmp),
361
                Finder::create()
362
                    ->files()
363
                    ->ignoreDotFiles(false)
364
                    ->exclude('.phar')
365
                    ->in($tmp),
366
            ),
367
        );
368
369
        $meta = PharMeta::fromJson(FS::getFileContents($tmp.DIRECTORY_SEPARATOR.Extract::PHAR_META_PATH));
370
        unset($dumpedFiles[Extract::PHAR_META_PATH]);
371
372
        return [$meta, $dumpedFiles];
373
    }
374
375
    /**
376
     * @param array<string, array{'compression': CompressionAlgorithm, compressedSize: int}> $filesMeta
377
     */
378
    private static function calculateCompressionCount(array $filesMeta): array
379
    {
380
        $count = array_fill_keys(
381
            self::$ALGORITHMS,
382
            0,
383
        );
384
385
        foreach ($filesMeta as ['compression' => $compression]) {
386
            ++$count[$compression->name];
387
        }
388
389
        return $count;
390
    }
391
}
392