<?php 
declare(strict_types=1); 
namespace ParagonIE\Pharaoh; 
use ParagonIE\ConstantTime\Hex; 
 
/** 
 * Class PharDiff 
 * @package ParagonIE\Pharaoh 
 */ 
class PharDiff 
{ 
    /** 
     * @var array<string, string> 
     */ 
    protected $c = [ 
        '' => "\033[0;39m", 
        'red'       => "\033[0;31m", 
        'green'     => "\033[0;32m", 
        'blue'      => "\033[1;34m", 
        'cyan'      => "\033[1;36m", 
        'silver'    => "\033[0;37m", 
        'yellow'    => "\033[0;93m" 
    ]; 
 
    /** @var array<int, Pharaoh> */ 
    private $phars = []; 
 
    /** @var bool $verbose */ 
    private $verbose = false; 
     
    /** 
     * Constructor uses dependency injection. 
     *  
     * @param \ParagonIE\Pharaoh\Pharaoh $pharA 
     * @param \ParagonIE\Pharaoh\Pharaoh $pharB 
     */ 
    public function __construct(Pharaoh $pharA, Pharaoh $pharB) 
    { 
        $this->phars = [$pharA, $pharB]; 
    } 
     
    /** 
     * Prints a git-formatted diff of the two phars. 
     * 
     * @psalm-suppress ForbiddenCode 
     * @return int 
     */ 
    public function printGitDiff(): int 
    { 
        // Lazy way; requires git. Will replace with custom implementaiton later. 
         
        $argA = \escapeshellarg($this->phars[0]->tmp); 
        $argB = \escapeshellarg($this->phars[1]->tmp); 
        /** @var string $diff */ 
        $diff = `git diff --no-index $argA $argB`; 
        echo $diff; 
        if (empty($diff) && $this->verbose) { 
            echo 'No differences encountered.', PHP_EOL; 
            return 0; 
        } 
        return 1; 
    } 
     
    /** 
     * Prints a GNU diff of the two phars. 
     * 
     * @psalm-suppress ForbiddenCode 
     * @return int 
     */ 
    public function printGnuDiff(): int 
    { 
        // Lazy way. Will replace with custom implementaiton later. 
        $argA = \escapeshellarg($this->phars[0]->tmp); 
        $argB = \escapeshellarg($this->phars[1]->tmp); 
        /** @var string $diff */ 
        $diff = `diff $argA $argB`; 
        echo $diff; 
        if (empty($diff) && $this->verbose) { 
            echo 'No differences encountered.', PHP_EOL; 
            return 0; 
        } 
        return 1; 
    } 
     
    /** 
     * Get hashes of all of the files in the two arrays. 
     *  
     * @param string $algo 
     * @param string $dirA 
     * @param string $dirB 
     * @return array<int, array<mixed, string>> 
     * @throws \SodiumException 
     */ 
    public function hashChildren(string $algo,string  $dirA, string $dirB) 
    { 
        /** 
         * @var string $a 
         * @var string $b 
         */ 
        $a = $b = ''; 
        $filesA = $this->listAllFiles($dirA); 
        $filesB = $this->listAllFiles($dirB); 
        $numFiles = \max(\count($filesA), \count($filesB)); 
         
        // Array of two empty arrays 
        $hashes = [[], []]; 
        for ($i = 0; $i < $numFiles; ++$i) { 
            $thisFileA = (string) $filesA[$i]; 
            $thisFileB = (string) $filesB[$i]; 
            if (isset($filesA[$i])) { 
                $a = \preg_replace('#^'.\preg_quote($dirA, '#').'#', '', $thisFileA); 
                if (isset($filesB[$i])) { 
                    $b = \preg_replace('#^'.\preg_quote($dirB, '#').'#', '', $thisFileB); 
                } else { 
                    $b = $a; 
                } 
            } elseif (isset($filesB[$i])) { 
                $b = \preg_replace('#^'.\preg_quote($dirB, '#').'#', '', $thisFileB); 
                $a = $b; 
            } 
             
            if (isset($filesA[$i])) { 
                // A exists 
                if (\strtolower($algo) === 'blake2b') { 
                    $hashes[0][$a] = Hex::encode(\ParagonIE_Sodium_File::generichash($thisFileA)); 
                } else { 
                    $hashes[0][$a] = \hash_file($algo, $thisFileA); 
                } 
            } elseif (isset($filesB[$i])) { 
                // A doesn't exist, B does 
                $hashes[0][$a] = ''; 
            } 
             
            if (isset($filesB[$i])) { 
                // B exists 
                if (\strtolower($algo) === 'blake2b') { 
                    $hashes[1][$b] = Hex::encode(\ParagonIE_Sodium_File::generichash($thisFileB)); 
                } else { 
                    $hashes[1][$b] = \hash_file($algo, $thisFileB); 
                } 
            } elseif (isset($filesA[$i])) { 
                // B doesn't exist, A does 
                $hashes[1][$b] = ''; 
            } 
        } 
        return $hashes; 
    } 
     
     
    /** 
     * List all the files in a directory (and subdirectories) 
     * 
     * @param string $folder - start searching here 
     * @param string $extension - extensions to match 
     * @return array 
     */ 
    private function listAllFiles($folder, $extension = '*') 
    { 
        /** 
         * @var array<mixed, string> $fileList 
         * @var string $i 
         * @var string $file 
         * @var \RecursiveDirectoryIterator $dir 
         * @var \RecursiveIteratorIterator $ite 
         */ 
        $dir = new \RecursiveDirectoryIterator($folder); 
        $ite = new \RecursiveIteratorIterator($dir); 
        if ($extension === '*') { 
            $pattern = '/.*/'; 
        } else { 
            $pattern = '/.*\.' . $extension . '$/'; 
        } 
        $files = new \RegexIterator($ite, $pattern, \RegexIterator::GET_MATCH); 
 
        /** @var array<string, string> $fileList */ 
        $fileList = []; 
 
        /** 
         * @var string $fileSub 
         */ 
        foreach($files as $fileSub) { 
            $fileList = \array_merge($fileList, $fileSub); 
        } 
 
        /** 
         * @var string $i 
         * @var string $file 
         */ 
        foreach ($fileList as $i => $file) { 
            if (\preg_match('#/\.{1,2}$#', (string) $file)) { 
                unset($fileList[$i]); 
            } 
        } 
        return \array_values($fileList); 
    } 
 
    /** 
     * Prints out all of the differences of checksums of the files contained 
     * in both PHP archives. 
     * 
     * @param string $algo 
     * @return int 
     * @throws \SodiumException 
     */ 
    public function listChecksums(string $algo = 'sha384'): int 
    { 
        list($pharA, $pharB) = $this->hashChildren( 
            $algo, 
            $this->phars[0]->tmp, 
            $this->phars[1]->tmp 
        ); 
 
        $diffs = 0; 
        /** @var string $i */ 
        foreach (\array_keys($pharA) as $i) { 
            if (isset($pharA[$i]) && isset($pharB[$i])) { 
                // We are NOT concerned about local timing attacks. 
                if ($pharA[$i] !== $pharB[$i]) { 
                    ++$diffs; 
                    echo "\t", (string) $i, 
                    "\n\t\t", $this->c['red'], $pharA[$i], $this->c[''], 
                    "\t", $this->c['green'], $pharB[$i], $this->c[''], 
                    "\n"; 
                } elseif (!empty($pharA[$i]) && empty($pharB[$i])) { 
                    ++$diffs; 
                    echo "\t", (string) $i, 
                    "\n\t\t", $this->c['red'], $pharA[$i], $this->c[''], 
                    "\t", \str_repeat('-', \strlen($pharA[$i])), 
                    "\n"; 
                } elseif (!empty($pharB[$i]) && empty($pharA[$i])) { 
                    ++$diffs; 
                    echo "\t", (string) $i, 
                    "\n\t\t", \str_repeat('-', \strlen($pharB[$i])), 
                    "\t", $this->c['green'], $pharB[$i], $this->c[''], 
                    "\n"; 
                } 
            } 
        } 
        if ($diffs === 0) { 
            if ($this->verbose) { 
                echo 'No differences encountered.', PHP_EOL; 
            } 
            return 0; 
        } 
        return 1; 
    } 
 
    /** 
     * Verbose mode says something when there are no differences. 
     * By default, you can just check the return value. 
     * 
     * @param bool $value 
     * @return void 
     */ 
    public function setVerbose(bool $value) 
    { 
        $this->verbose = $value; 
    } 
} 
 
 |