diff --git a/src/ExecutableFinder.php b/src/ExecutableFinder.php new file mode 100644 index 000000000..f692d0cde --- /dev/null +++ b/src/ExecutableFinder.php @@ -0,0 +1,52 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use RuntimeException; +use Symfony\Component\Process\PhpExecutableFinder as SymfonyPhpExecutableFinder; + +final class ExecutableFinder +{ + private static string $boxExecutable; + private static string $phpExecutable; + + public static function findBoxExecutable(): string + { + if (isset(self::$boxExecutable)) { + return self::$boxExecutable; + } + + self::$boxExecutable = getenv(Constants::BIN) ?: $_SERVER['SCRIPT_NAME']; + + return self::$boxExecutable; + } + + public static function findPhpExecutable(): string + { + if (isset(self::$phpExecutable)) { + return self::$phpExecutable; + } + + $phpExecutable = (new SymfonyPhpExecutableFinder())->find(); + + if (false === $phpExecutable) { + throw new RuntimeException('Could not find a PHP executable.'); + } + + self::$phpExecutable = $phpExecutable; + + return self::$phpExecutable; + } +} diff --git a/src/Phar/PharInfo.php b/src/Phar/PharInfo.php index 5173c04a2..9a7be3929 100644 --- a/src/Phar/PharInfo.php +++ b/src/Phar/PharInfo.php @@ -45,19 +45,16 @@ use Fidry\FileSystem\FS; use KevinGH\Box\Console\Command\Extract; -use KevinGH\Box\Constants; +use KevinGH\Box\ExecutableFinder; use OutOfBoundsException; use Phar; -use RuntimeException; use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use function bin2hex; use function file_exists; -use function getenv; use function is_readable; use function iter\mapKeys; use function iter\toArrayWithKeys; @@ -288,8 +285,8 @@ private static function initStubFileName(): void private static function dumpPhar(string $file, string $tmp): void { $extractPharProcess = new Process([ - self::getPhpExecutable(), - self::getBoxBin(), + ExecutableFinder::findPhpExecutable(), + ExecutableFinder::findBoxExecutable(), 'extract', $file, $tmp, @@ -329,28 +326,6 @@ private static function loadDumpedPharFiles(string $tmp): array return [$meta, $dumpedFiles]; } - private static function getPhpExecutable(): string - { - if (isset(self::$phpExecutable)) { - return self::$phpExecutable; - } - - $phpExecutable = (new PhpExecutableFinder())->find(); - - if (false === $phpExecutable) { - throw new RuntimeException('Could not find a PHP executable.'); - } - - self::$phpExecutable = $phpExecutable; - - return self::$phpExecutable; - } - - private static function getBoxBin(): string - { - return getenv(Constants::BIN) ?: $_SERVER['SCRIPT_NAME']; - } - /** * @param array $filesMeta */ diff --git a/src/PharInfo/PharInfo.php b/src/PharInfo/PharInfo.php index 71bbffa84..27199a5f6 100644 --- a/src/PharInfo/PharInfo.php +++ b/src/PharInfo/PharInfo.php @@ -14,9 +14,11 @@ namespace KevinGH\Box\PharInfo; -use KevinGH\Box\Phar\PharInfo as NewPharInfo; +use KevinGH\Box\Phar\CompressionAlgorithm; use Phar; use PharData; +use PharFileInfo; +use RecursiveIteratorIterator; use UnexpectedValueException; use function KevinGH\Box\unique_id; use function realpath; @@ -27,35 +29,56 @@ */ final class PharInfo { - private NewPharInfo $decoratedPharInfo; + private static array $ALGORITHMS; private PharData|Phar $phar; - public function __construct(private readonly string $pharFile) + private ?array $compressionCount = null; + private ?string $hash = null; + + public function __construct(string $pharFile) + { + self::initAlgorithms(); + + try { + $this->phar = new Phar($pharFile); + } catch (UnexpectedValueException) { + $this->phar = new PharData($pharFile); + } + } + + private static function initAlgorithms(): void { - $this->decoratedPharInfo = new NewPharInfo($pharFile); + if (!isset(self::$ALGORITHMS)) { + self::$ALGORITHMS = []; + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + self::$ALGORITHMS[$compressionAlgorithm->value] = $compressionAlgorithm->name; + } + } } public function equals(self $pharInfo): bool { - return $this->decoratedPharInfo->equals($pharInfo->decoratedPharInfo); + return + $pharInfo->getCompressionCount() === $this->getCompressionCount() + && $pharInfo->getNormalizedMetadata() === $this->getNormalizedMetadata(); } public function getCompressionCount(): array { - return $this->decoratedPharInfo->getFilesCompressionCount(); + if (null === $this->compressionCount || $this->hash !== $this->getPharHash()) { + $this->compressionCount = $this->calculateCompressionCount(); + $this->compressionCount['None'] = $this->compressionCount[CompressionAlgorithm::NONE->name]; + unset($this->compressionCount[CompressionAlgorithm::NONE->name]); + $this->hash = $this->getPharHash(); + } + + return $this->compressionCount; } public function getPhar(): Phar|PharData { - if (!isset($this->phar)) { - try { - $this->phar = new Phar($this->pharFile); - } catch (UnexpectedValueException) { - $this->phar = new PharData($this->pharFile); - } - } - return $this->phar; } @@ -67,16 +90,60 @@ public function getRoot(): string public function getVersion(): string { - return $this->decoratedPharInfo->getVersion(); + // Do not cache the result + return '' !== $this->phar->getVersion() ? $this->phar->getVersion() : 'No information found'; } public function getNormalizedMetadata(): ?string { - return $this->decoratedPharInfo->getNormalizedMetadata(); + // Do not cache the result + $metadata = var_export($this->phar->getMetadata(), true); + + return 'NULL' === $metadata ? null : $metadata; } private function getPharHash(): string { - return $this->decoratedPharInfo->getSignature()['hash'] ?? unique_id(''); + // If no signature is available (e.g. a tar.gz file), we generate a random hash to ensure + // it will always be invalidated + return $this->phar->getSignature()['hash'] ?? unique_id(''); + } + + private function calculateCompressionCount(): array + { + $count = array_fill_keys( + self::$ALGORITHMS, + 0, + ); + + if ($this->phar instanceof PharData) { + $count[self::$ALGORITHMS[$this->phar->isCompressed()]] = 1; + + return $count; + } + + $countFile = static function (array $count, PharFileInfo $file): array { + if (false === $file->isCompressed()) { + ++$count[CompressionAlgorithm::NONE->name]; + + return $count; + } + + foreach (self::$ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) { + if ($file->isCompressed($compressionAlgorithmCode)) { + ++$count[$compressionAlgorithmName]; + + return $count; + } + } + + return $count; + }; + + return array_reduce( + iterator_to_array(new RecursiveIteratorIterator($this->phar), true), + $countFile, + $count, + ); } }