#!/bin/php
<?php
// RDEPEND: app-misc/pax-utils
// need actual pwphpbytecode and actual pcc

$sysids = getPwphpbytecodeZendIdTools();
print_r($sysids);

$results_ok =array();
$results_nok=array();

$unmute=false;
if (isset($argv[1]) && $argv[1] == "verbose") {
    $unmute=true;
}


$exec = findInstalledPackagesRequiringPwphpbytecode();
foreach($exec as $line){
    $line=trim($line);
    if(empty($line)) continue;
    if($unmute) echo "Checking ".$line."...\r\n";
    $elf = get_elf_for_pkg($line);
    foreach($elf as $file){
        $dependon = getPwphpbytecodeLibForElf($file);
        if(stripos($dependon,"_raw-")!==false) continue;

        if($unmute) echo $file." -> ".$dependon."\r\n";
        $elf_info=getPccEmbedfileInfoFromElf($file);
        $lib_sysid=get_system_id($sysids,$dependon);
        if($unmute) echo "libphp.so Zend-System-ID: ".$lib_sysid[0]." (PHPv".$lib_sysid[1].")\r\n";
        if($unmute) echo "File (".$file.") Identifier: PCCv".$elf_info["pcc_version"]." PHPv".$elf_info["php_version"]." Zend-System-ID: ".$elf_info["zend_system_id"]."\r\n";
        if($unmute) echo "\r\n";
        if($elf_info["zend_system_id"]==$lib_sysid[0] && $elf_info["php_version"]==$lib_sysid[1]){
            $results_ok[]=$line.": ".$file;
        }
        else{
            $results_nok[]=array("pkg"=>$line,"file"=>$file,"should_php_version"=>$lib_sysid[1],"is_for_php_version"=>$elf_info["php_version"],"should_zend_system_id"=>$lib_sysid[0],"is_for_zend_system_id"=>$elf_info["zend_system_id"]);
        }
    }
    if($unmute) echo "\r\n";
    if($unmute) echo "\r\n";
}

if($unmute) {
    echo "OK Packages/Files:\r\n";
    print_r($results_ok);
}

echo "Possible broken:\r\n";
print_r($results_nok);

$rebuild=array();
foreach($results_nok as $pkg){
    if(!in_array($pkg["pkg"],$rebuild)){
        $rebuild[]=$pkg["pkg"];
    }
}

echo "Should be rebuild:\r\n";
print_r($rebuild);



$checked = splitExistingAndMissingPortageAtoms($rebuild);

echo "Can be rebuilt:\n";
print_r($checked['existing']);

echo "\nMissing from repo:\n";
print_r($checked['missing']);



if(count($checked['existing'])>0) {
    $emerge = "FEATURES=-getbinpkg emerge --oneshot";
    foreach ($checked["existing"] as $pkg) {
        $emerge .= " =" . $pkg;
    }
    echo "EMERGE: " . $emerge . "\r\n";
    echo "CALL " . $argv[0] . " repair to repair the packages!\r\n";

    if (isset($argv[1]) && $argv[1] == "repair") {
        echo "Repairing...\r\n";
        passthru($emerge);
    }
}
echo "DONE!\r\n";


function get_system_id($sysids,$lib){
    foreach($sysids as $sysid){
        if($sysid["libpwphpbytecode"]==$lib){
            return(array($sysid["zend_system_id"],$sysid["php_version"]));
        }
    }
    return("unknown");
}


function get_elf_for_pkg($pkg){
    $scanelfOutput = getPackageElfScanelfNeededOutput($pkg);
    $files = scanelfOutputToFiles($scanelfOutput);
    return($files);
}


function scanelfOutputToFiles(array $lines, bool $basenameOnly = false): array
{
    $files = [];

    foreach ($lines as $line) {
        $line = trim($line);

        if ($line === '' || str_starts_with($line, 'TYPE ')) {
            continue;
        }

        $parts = preg_split('/\s+/', $line);
        $file = end($parts);

        $files[] = $basenameOnly ? basename($file) : $file;
    }

    return $files;
}

function getPwphpbytecodeLibForElf(string $file): ?string
{
    if (!is_file($file)) {
        throw new InvalidArgumentException("Datei existiert nicht: {$file}");
    }

    $cmd = 'scanelf -n ' . escapeshellarg($file) . ' 2>/dev/null';
    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0 || $output === []) {
        return null;
    }

    foreach ($output as $line) {
        $line = trim($line);

        if ($line === '' || str_starts_with($line, 'TYPE ')) {
            continue;
        }

        /*
         * scanelf -n Format:
         * TYPE   NEEDED                FILE
         * ET_DYN libfoo.so,libbar.so   /path/to/file
         */
        $parts = preg_split('/\s+/', $line, 3);

        if (count($parts) < 3) {
            continue;
        }

        $neededRaw = $parts[1];
        $neededLibs = array_filter(explode(',', $neededRaw));

        foreach ($neededLibs as $lib) {
            if (preg_match('/^libpwphpbytecode.*\.so$/', $lib)) {
                return $lib;
            }
        }
    }

    return null;
}
function getPccEmbedfileInfoFromElf(string $file): ?array
{
    if (!is_file($file)) {
        throw new InvalidArgumentException("Datei existiert nicht: {$file}");
    }

    $data = file_get_contents($file);

    if ($data === false) {
        throw new RuntimeException("Datei konnte nicht gelesen werden: {$file}");
    }

    $pattern = '/\bCopyright by S\. Peters, compiled by pcc_embedfile_bytecode v([0-9]+\.[0-9]+) for PHP v([0-9]+\.[0-9]+\.[0-9]+)(?: \(Zend-System-ID: ([a-fA-F0-9]{32})\))?/';

    if (!preg_match($pattern, $data, $m)) {
        return null;
    }

    return [
        'pcc_version'    => $m[1],
        'php_version'    => $m[2],
        'zend_system_id' => isset($m[3]) && $m[3] !== '' ? strtolower($m[3]) : 'unknown',
    ];
}

function getPwphpbytecodeZendIdTools(string $glob = '/bin/pwphpbytecode_zendid_*'): array
{
    $tools = glob($glob) ?: [];
    sort($tools);

    $result = [];

    foreach ($tools as $tool) {
        if (!is_file($tool) || !is_executable($tool)) {
            continue;
        }

        $info = readPwphpbytecodeZendidTool($tool);

        if ($info === null) {
            continue;
        }

        $lib = getLinkedPwphpbytecodeLibFromLddtree($tool);

        if ($lib === null) {
            continue;
        }

        $info['tool'] = $tool;
        $info['libpwphpbytecode'] = $lib['soname'];
        $info['libpwphpbytecode_path'] = $lib['path'];

        $result[] = $info;
    }

    return $result;
}

function readPwphpbytecodeZendidTool(string $tool): ?array
{
    $cmd = escapeshellarg($tool) . ' 2>/dev/null';
    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0 || $output === []) {
        return null;
    }

    $info = [];

    foreach ($output as $line) {
        $line = trim($line);

        if (preg_match('/^PHP_VERSION=(.+)$/', $line, $m)) {
            $info['php_version'] = $m[1];
            continue;
        }

        if (preg_match('/^ZEND_VERSION=(.+)$/', $line, $m)) {
            $info['zend_version'] = $m[1];
            continue;
        }

        if (preg_match('/^ZEND_SYSTEM_ID=([a-fA-F0-9]{32})$/', $line, $m)) {
            $info['zend_system_id'] = strtolower($m[1]);
            continue;
        }
    }

    if (
        empty($info['php_version']) ||
        empty($info['zend_version']) ||
        empty($info['zend_system_id'])
    ) {
        return null;
    }

    return $info;
}

function getLinkedPwphpbytecodeLibFromLddtree(string $file): ?array
{
    $cmd = 'lddtree ' . escapeshellarg($file) . ' 2>/dev/null';
    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0 || $output === []) {
        return null;
    }

    foreach ($output as $line) {
        $line = trim($line);

        /*
         * Beispiel:
         * libpwphpbytecode-8.3-3.so => /usr/lib64/libpwphpbytecode-8.3-3.so
         */
        if (preg_match('/^(libpwphpbytecode(?:_raw)?-[^ \t]+\.so)\s+=>\s+(.+)$/', $line, $m)) {
            return [
                'soname' => $m[1],
                'path'   => $m[2],
            ];
        }
    }

    return null;
}
function findInstalledPackagesRequiringPwphpbytecode(
    string $vdbPath = '/var/db/pkg',
    string $needle = 'libpwphpbytecode'
): array {
    $packages = [];

    foreach (glob($vdbPath . '/*/*/REQUIRES') ?: [] as $requiresFile) {
        $content = file_get_contents($requiresFile);

        if ($content === false) {
            continue;
        }

        if (!str_contains($content, $needle)) {
            continue;
        }

        // /var/db/pkg/category/package-version/REQUIRES
        $packageDir = dirname($requiresFile);
        $categoryDir = dirname($packageDir);

        $category = basename($categoryDir);
        $package = basename($packageDir);

        $packages[] = $category . '/' . $package;
    }

    sort($packages);

    return array_values(array_unique($packages));
}
function getPackageElfScanelfNeededOutput(string $pkg, string $vdbPath = '/var/db/pkg'): array
{
    $contentsFile = rtrim($vdbPath, '/') . '/' . ltrim($pkg, '/') . '/CONTENTS';

    if (!is_file($contentsFile)) {
        throw new InvalidArgumentException("CONTENTS-Datei nicht gefunden: {$contentsFile}");
    }

    $result = [];

    foreach (getPackageObjectFiles($contentsFile) as $file) {
        if (!isElfFile($file)) {
            continue;
        }

        $scanelfOutput = scanelfNeeded($file);

        foreach ($scanelfOutput as $line) {
            $result[] = $line;
        }
    }

    return $result;
}

function getPackageObjectFiles(string $contentsFile): array
{
    $files = [];

    $fh = fopen($contentsFile, 'rb');

    if ($fh === false) {
        throw new RuntimeException("Konnte CONTENTS nicht öffnen: {$contentsFile}");
    }

    try {
        while (($line = fgets($fh)) !== false) {
            $line = trim($line);

            if ($line === '') {
                continue;
            }

            /*
             * CONTENTS-Format:
             * obj /usr/bin/foo md5 timestamp
             * sym /usr/bin/bar -> foo timestamp
             * dir /usr/bin
             */
            $parts = preg_split('/\s+/', $line, 3);

            if (($parts[0] ?? null) !== 'obj') {
                continue;
            }

            if (!isset($parts[1])) {
                continue;
            }

            $files[] = $parts[1];
        }
    } finally {
        fclose($fh);
    }

    return $files;
}

function isElfFile(string $file): bool
{
    if (!is_file($file)) {
        return false;
    }

    $fh = fopen($file, 'rb');

    if ($fh === false) {
        return false;
    }

    try {
        $magic = fread($fh, 4);
        return $magic === "\x7fELF";
    } finally {
        fclose($fh);
    }
}

function scanelfNeeded(string $file): array
{
    $cmd = 'scanelf -n ' . escapeshellarg($file) . ' 2>/dev/null';

    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0) {
        return [];
    }

    return $output;
}
function portageAtomExists(string $cpv): bool
{
    $atom = '=' . $cpv;

    $cmd = 'portageq best_visible / ' . escapeshellarg($atom) . ' 2>/dev/null';

    exec($cmd, $output, $exitCode);

    if ($exitCode !== 0) {
        return false;
    }

    return count($output) > 0;
}

function filterExistingPortageAtoms(array $cpvs): array
{
    $existing = [];

    foreach ($cpvs as $cpv) {
        $cpv = trim($cpv);

        if ($cpv === '') {
            continue;
        }

        if (portageAtomExists($cpv)) {
            $existing[] = $cpv;
        }
    }

    return $existing;
}

function splitExistingAndMissingPortageAtoms(array $cpvs): array
{
    $existing = [];
    $missing = [];

    foreach ($cpvs as $cpv) {
        $cpv = trim($cpv);

        if ($cpv === '') {
            continue;
        }

        if (portageAtomExists($cpv)) {
            $existing[] = $cpv;
        } else {
            $missing[] = $cpv;
        }
    }

    sort($existing);
    sort($missing);

    return [
        'existing' => $existing,
        'missing'  => $missing,
    ];
}
