<?php







class EasyChatOpenCartRuntimePublisher {
    const MANIFEST_FORMAT = 1;
    const PUBLISH_REVISION = '4';
    const INTENT_STALE_SECONDS = 120;
    const LEGACY_DRAIN_SECONDS = 240;

    private $lockWaitMilliseconds;
    private $lastError = '';
    private $legacyDrainReadyAt = 0;
    private $legacyDrainResumeToken = '';

    public function __construct($lockWaitMilliseconds = 70000) {
        $this->lockWaitMilliseconds = max(0, min(75000, (int)$lockWaitMilliseconds));
    }

    public function lastError() {
        return $this->lastError;
    }

    public function legacyDrainReadyAt() {
        return $this->legacyDrainReadyAt;
    }

    public function legacyDrainResumeToken() {
        return $this->legacyDrainResumeToken;
    }

    public function publish($source, $runtime, $expectedVersion) {
        $this->lastError = '';
        $this->legacyDrainReadyAt = 0;
        $this->legacyDrainResumeToken = '';
        $source = rtrim((string)$source, '/\\');
        $runtime = rtrim((string)$runtime, '/\\');
        $expectedVersion = trim((string)$expectedVersion);
        $lock = null;
        $transaction = '';
        $statePrepared = false;
        $stateCommitted = false;
        $intentNonce = '';
        $legacyDrainArmed = false;
        $publicationSucceeded = false;

        try {
            if ($this->pathsOverlap($source, $runtime)) {
                return $this->fail('source_runtime_overlap');
            }
            if (!$this->prepareRuntimeStorage($runtime)) {
                return $this->fail('runtime_storage_unavailable');
            }
            if (!$this->ensureExecutionWindow()) {
                return $this->fail('execution_window_unavailable');
            }

            $intentNonce = $this->beginWriterIntent($runtime);
            if ($intentNonce === '') {
                return $this->fail('publish_intent_unavailable');
            }

            $lock = $this->acquireLock($runtime, function () use ($runtime, $intentNonce) {
                return $this->refreshWriterIntent($runtime, $intentNonce);
            });
            if (!is_resource($lock)) {
                return $this->fail('publish_lock_unavailable');
            }
            if (!$this->writerIntentOwned($runtime, $intentNonce)) {
                return $this->fail('publish_intent_lost');
            }
            if (!$this->recoverInterruptedPublication($runtime)) {
                return $this->fail('interrupted_publish_recovery_failed');
            }
            if (!$this->cleanupOrphanTransactions($runtime)) {
                return $this->fail('orphan_transaction_cleanup_failed');
            }

             
             
             
             
            $sourceManifest = $this->verifiedBundledSourceManifest($source, $expectedVersion);
            if ($sourceManifest === false) {
                return $this->fail('bundled_source_incomplete');
            }
            $drainStatus = $this->ensureLegacyDrainGate($runtime, $expectedVersion);
            if ($drainStatus === false) {
                return $this->fail('legacy_drain_gate_failed');
            }
            if ($drainStatus > time()) {
                $this->legacyDrainReadyAt = $drainStatus;
                return $this->fail('legacy_drain_pending');
            }
            $legacyDrainArmed = $drainStatus > 0;
            $transaction = $this->createTransaction($runtime);
            if ($transaction === '') {
                return $this->fail('transaction_create_failed');
            }

            $stage = $transaction . '/stage';
            $backup = $transaction . '/backup';
            if (!$this->createDirectory($stage, 0700)
                || !$this->createDirectory($backup, 0700)
                || !$this->materializeManifestBlobs($source, $stage, $sourceManifest, true)
                || !$this->treeMatchesManifest($stage, $sourceManifest, false)
            ) {
                return $this->fail('stage_prepare_failed');
            }

            $backupManifest = $this->buildTreeManifest($runtime, true);
            $metadataManifest = $this->snapshotPublicationMetadata($runtime, $transaction);
            if ($backupManifest === false
                || $metadataManifest === false
                || !$this->materializeManifestBlobs($runtime, $backup, $backupManifest)
                || !$this->treeMatchesManifest($backup, $backupManifest, false)
                || !$this->writeJsonAtomic($transaction . '/source-manifest.json', $sourceManifest, 0600)
                || !$this->writeJsonAtomic($transaction . '/backup-manifest.json', $backupManifest, 0600)
            ) {
                return $this->fail('backup_prepare_failed');
            }

             
             
             
            $growthBytes = $this->manifestBytes($sourceManifest)
                + $this->largestManifestFile($sourceManifest)
                + 1024 * 1024;
            $rollbackBytes = $this->manifestBytes($backupManifest)
                + $this->manifestBytes($metadataManifest)
                + $this->largestManifestFile($backupManifest)
                + $this->largestManifestFile($metadataManifest)
                + 1024 * 1024;
            if (!$this->createAllocatedReserve($transaction . '/growth-reserve.bin', $growthBytes)
                || !$this->createAllocatedReserve($transaction . '/rollback-reserve.bin', $rollbackBytes)
            ) {
                return $this->fail('rollback_reserve_unavailable');
            }
            if (!$this->removeFile($transaction . '/growth-reserve.bin')) {
                return $this->fail('growth_reserve_release_failed');
            }
            if (!$this->clearRuntimeClassmapCaches($runtime)) {
                return $this->fail('classmap_cleanup_failed');
            }

            $state = array(
                'format' => self::MANIFEST_FORMAT,
                'transaction' => basename($transaction),
                'phase' => 'prepared',
                'expected_version' => $expectedVersion,
                'source_digest' => $sourceManifest['digest'],
            );
            if (!$this->writeJsonAtomic($this->statePath($runtime), $state, 0600)) {
                return $this->fail('publish_state_write_failed');
            }
            $statePrepared = true;
            $this->checkpoint('after_state_prepared');

            if (!$this->applyManifest($stage, $runtime, $sourceManifest)
                || !$this->removeFilesMissingFromManifest($runtime, $sourceManifest)
                || !$this->treeMatchesManifest($runtime, $sourceManifest, true)
            ) {
                throw new \RuntimeException('runtime_commit_failed');
            }
            $this->checkpoint('after_runtime_commit');

            $publishedManifest = $sourceManifest;
            $publishedManifest['format'] = self::MANIFEST_FORMAT;
            $publishedManifest['version'] = $expectedVersion;
            $publishedManifest['publish_revision'] = self::PUBLISH_REVISION;
            if (!$this->writeJsonAtomic($this->publishedManifestPath($runtime), $publishedManifest, 0640)) {
                throw new \RuntimeException('published_manifest_write_failed');
            }

             
             
             
            $this->checkpoint('before_managed_runtime_marker');
            if (!$this->writeManagedRuntimeMarkers($runtime)) {
                throw new \RuntimeException('managed_runtime_marker_failed');
            }
            $this->checkpoint('after_managed_runtime_marker');

            $state['phase'] = 'committed';
            if (!$this->writeJsonAtomic($this->statePath($runtime), $state, 0600)) {
                throw new \RuntimeException('committed_state_write_failed');
            }
            $stateCommitted = true;
            $this->checkpoint('after_committed_state');

            if (!$this->removeFile($transaction . '/rollback-reserve.bin')) {
                return $this->fail('rollback_reserve_release_failed');
            }
            if (!$this->removeFile($this->statePath($runtime))) {
                return $this->fail('publish_state_cleanup_failed');
            }
            $statePrepared = false;
            $this->removeTree($transaction);

            if (!$this->clearLegacyDrainMetadata($runtime)) {
                return $this->fail('legacy_drain_cleanup_failed');
            }

            $publicationSucceeded = true;
            return true;
        } catch (\Throwable $e) {
            $this->lastError = $e->getMessage() !== '' ? $e->getMessage() : 'publish_failed';
            if ($statePrepared && !$stateCommitted) {
                if ($this->restoreTransactionBackup($runtime, $transaction)) {
                    $this->removeFile($this->statePath($runtime));
                    $this->removeTree($transaction);
                } else {
                    $this->lastError .= ':rollback_failed';
                }
            }
             
             
            return false;
        } finally {
            if (!$statePrepared && $transaction !== '' && is_dir($transaction)) {
                $this->removeTree($transaction);
            }
            if ($legacyDrainArmed
                && !$publicationSucceeded
                && !file_exists($this->statePath($runtime))
                && !is_link($this->statePath($runtime))
                && !$this->runtimeHasManagedRevision($runtime)
                && !$this->restoreAndClearLegacyDrain($runtime, $expectedVersion)
            ) {
                $this->lastError .= ($this->lastError === '' ? '' : ':') . 'legacy_drain_restore_failed';
            }
            if (is_resource($lock)) {
                $this->releaseLock($lock);
            }
            if ($intentNonce !== '') {
                $this->endWriterIntent($runtime, $intentNonce);
            }
        }
    }

    public function isReady($runtime, $expectedVersion, $publishRevision, array $requiredFiles, $verifyFullTree = false) {
        $runtime = rtrim((string)$runtime, '/\\');
        $manifest = $this->readJson($this->publishedManifestPath($runtime));
        if (!$this->validManifest($manifest)
            || (int)($manifest['format'] ?? 0) !== self::MANIFEST_FORMAT
            || !hash_equals((string)$expectedVersion, (string)($manifest['version'] ?? ''))
            || !hash_equals((string)$publishRevision, (string)($manifest['publish_revision'] ?? ''))
            || is_file($this->statePath($runtime))
            || is_link($this->statePath($runtime))
            || is_file($this->intentPath($runtime))
            || is_link($this->intentPath($runtime))
            || !$this->validManagedRuntimeMarker($runtime)
        ) {
            return false;
        }

        foreach ($requiredFiles as $relativePath) {
            $relativePath = $this->normalizeRelativePath($relativePath);
            if ($relativePath === ''
                || !isset($manifest['files'][$relativePath])
                || !$this->fileMatchesEntry($runtime . '/' . $relativePath, $manifest['files'][$relativePath])
            ) {
                return false;
            }
        }

        return !$verifyFullTree || $this->treeMatchesManifest($runtime, $manifest, true);
    }

    public function canCompleteBundledSource($source) {
        $this->lastError = '';
        $source = rtrim((string)$source, '/\\');
        $expectation = $this->readBundleExpectation($source);
        if ($expectation === false) {
            return $this->fail('bundle_expectation_invalid');
        }
        $definition = $this->readVerifiedBundleDefinition($source, $expectation['version']);
        $actual = $this->buildTreeManifest($source, true, true);
        $actualContent = $actual === false ? false : $this->contentOnlyManifest($actual);
        if ($definition === false
            || $actualContent === false
            || !$this->sourceVersionMatches($source, $expectation['version'])
        ) {
            return $this->fail('bundled_source_incomplete');
        }
        foreach ($definition['manifest']['files'] as $relativePath => $entry) {
            if (!isset($actualContent['files'][$relativePath])
                || $actualContent['files'][$relativePath] !== $entry
            ) {
                return $this->fail('bundled_source_incomplete');
            }
        }
        return true;
    }

    public function canCompleteAdapterDelivery(array $roots, $expectationFile, $expectedVersion) {
        $this->lastError = '';
        $expectedVersion = trim((string)$expectedVersion);
        $canonicalRoots = array();
        foreach (array('admin', 'catalog', 'system') as $prefix) {
            $root = isset($roots[$prefix]) ? rtrim((string)$roots[$prefix], '/\\') : '';
            $canonical = $root === '' ? false : $this->safeCall(function () use ($root) {
                return realpath($root);
            }, false);
            if (!is_string($canonical) || $canonical === '' || !is_dir($root) || is_link($root)) {
                return $this->fail('adapter_root_invalid');
            }
            $roots[$prefix] = $root;
            $canonicalRoots[$prefix] = rtrim($canonical, '/\\');
        }
        $expectationCanonical = $this->safeCall(function () use ($expectationFile) {
            return realpath($expectationFile);
        }, false);
        if (!is_string($expectationCanonical) || $expectationCanonical === ''
            || !is_file($expectationFile) || is_link($expectationFile)
            || strpos($expectationCanonical, $canonicalRoots['system'] . DIRECTORY_SEPARATOR) !== 0
        ) {
            return $this->fail('adapter_expectation_invalid');
        }
        $expectation = $this->readJson($expectationFile);
        if (!is_array($expectation)
            || (int)($expectation['format'] ?? 0) !== 1
            || !hash_equals($expectedVersion, (string)($expectation['version'] ?? ''))
            || !hash_equals(self::PUBLISH_REVISION, (string)($expectation['publish_revision'] ?? ''))
            || !isset($expectation['digest'], $expectation['files'])
            || !is_string($expectation['digest'])
            || !preg_match('/^[a-f0-9]{64}$/', $expectation['digest'])
            || !is_array($expectation['files'])
        ) {
            return $this->fail('adapter_expectation_invalid');
        }
        $required = array(
            'admin/controller/extension/module/easychat.php',
            'admin/controller/extension/module/easychat/recovery_v3.php',
            'admin/language/en-gb/extension/module/easychat.php',
            'admin/language/es-es/extension/module/easychat.php',
            'admin/language/kk-kz/extension/module/easychat.php',
            'admin/language/ru-ru/extension/module/easychat.php',
            'admin/language/uk-ua/extension/module/easychat.php',
            'admin/language/uz-uz/extension/module/easychat.php',
            'admin/view/javascript/easychat/launch.js',
            'admin/view/template/extension/module/easychat.tpl',
            'admin/view/template/extension/module/easychat.twig',
            'admin/view/template/extension/module/easychat_launch.tpl',
            'admin/view/template/extension/module/easychat_launch.twig',
            'catalog/controller/extension/module/easychat.php',
            'catalog/view/theme/default/template/extension/module/easychat.tpl',
            'catalog/view/theme/default/template/extension/module/easychat.twig',
            'system/library/easychat_bundle_expectation.json',
            'system/library/easychat_adapter_runtime/admin/zz_current.json',
            'system/library/easychat_adapter_runtime/catalog/zz_current.json',
            'system/library/easychat_adapter_runtime/recovery/zz_current.json',
            'system/library/easychat_runtime_publisher.php',
            'system/library/easychat_runtime_publisher/zz_current.json',
        );
        $implementationPrefixes = array(
            'system/library/easychat_adapter_runtime/admin/00_impl_r' . self::PUBLISH_REVISION . '_',
            'system/library/easychat_adapter_runtime/catalog/00_impl_r' . self::PUBLISH_REVISION . '_',
            'system/library/easychat_adapter_runtime/recovery/00_impl_r' . self::PUBLISH_REVISION . '_',
            'system/library/easychat_runtime_publisher/00_impl_r' . self::PUBLISH_REVISION . '_',
        );
        $normalized = array();
        $implementationCounts = array_fill(0, count($implementationPrefixes), 0);
        foreach ($expectation['files'] as $relativePath => $entry) {
            $path = $this->normalizeRelativePath($relativePath);
            if ($path === '' || $path !== $relativePath
                || !is_array($entry) || count($entry) !== 2
                || !isset($entry['sha256'], $entry['size'])
                || !is_string($entry['sha256'])
                || !preg_match('/^[a-f0-9]{64}$/', $entry['sha256'])
                || !is_int($entry['size']) || $entry['size'] < 1
            ) {
                return $this->fail('adapter_expectation_invalid');
            }
            $allowed = in_array($path, $required, true);
            foreach ($implementationPrefixes as $index => $prefix) {
                if (strpos($path, $prefix) === 0
                    && preg_match('#^' . preg_quote($prefix, '#') . '([a-f0-9]{64})\\.php$#', $path, $matches)
                    && hash_equals($matches[1], $entry['sha256'])
                ) {
                    $implementationCounts[$index]++;
                    $allowed = true;
                }
            }
            if (!$allowed) {
                return $this->fail('adapter_expectation_invalid');
            }
            $segments = explode('/', $path, 2);
            if (count($segments) !== 2 || !isset($roots[$segments[0]])) {
                return $this->fail('adapter_expectation_invalid');
            }
            $rootPrefix = $segments[0];
            $file = $roots[$rootPrefix] . '/' . $segments[1];
            $canonical = $this->safeCall(function () use ($file) {
                return realpath($file);
            }, false);
            $size = $this->safeCall(function () use ($file) {
                return filesize($file);
            }, false);
            if (!is_string($canonical) || $canonical === ''
                || strpos($canonical, $canonicalRoots[$rootPrefix] . DIRECTORY_SEPARATOR) !== 0
                || !is_file($file) || is_link($file)
                || !is_int($size) || $size !== $entry['size']
                || !hash_equals($entry['sha256'], (string)$this->hashFile($file))
            ) {
                return $this->fail('adapter_delivery_incomplete:' . $path);
            }
            $normalized[$path] = $entry;
        }
        foreach ($required as $path) {
            if (!isset($normalized[$path])) {
                return $this->fail('adapter_expectation_invalid');
            }
        }
        if ($implementationCounts !== array(1, 1, 1, 1)) {
            return $this->fail('adapter_expectation_invalid');
        }
        ksort($normalized, SORT_STRING);
        $encoded = json_encode($normalized, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded) || !hash_equals($expectation['digest'], hash('sha256', $encoded))) {
            return $this->fail('adapter_expectation_digest_invalid');
        }

        $normalizedMigration = $this->normalizeMigrationExpectation($expectation, $normalized);
        if ($normalizedMigration === false) {
            return false;
        }
        foreach ($normalizedMigration as $path => $entry) {
            $segments = explode('/', $path, 2);
            $rootPrefix = $segments[0];
            $file = $roots[$rootPrefix] . '/' . $segments[1];
            if (file_exists($file) || is_link($file)) {
                $canonical = $this->safeCall(function () use ($file) {
                    return realpath($file);
                }, false);
                $size = $this->safeCall(function () use ($file) {
                    return filesize($file);
                }, false);
                if (!is_string($canonical) || $canonical === ''
                    || strpos($canonical, $canonicalRoots[$rootPrefix] . DIRECTORY_SEPARATOR) !== 0
                    || !is_file($file) || is_link($file)
                    || !is_int($size) || $size !== $entry['size']
                    || !hash_equals($entry['sha256'], (string)$this->hashFile($file))
                ) {
                    return $this->fail('adapter_migration_incomplete:' . $path);
                }
            }
        }
        return true;
    }

    







    public function cleanupObsoleteAdapterArtifacts(array $roots, $expectationFile, $expectedVersion) {
        if (!$this->canCompleteAdapterDelivery($roots, $expectationFile, $expectedVersion)) {
            return false;
        }

        $expectation = $this->readJson($expectationFile);
        if (!is_array($expectation) || !is_array($expectation['files'] ?? null)) {
            return $this->fail('adapter_cleanup_expectation_invalid');
        }
        $expected = array_fill_keys(array_keys($expectation['files']), true);
        $controllers = array(
            rtrim((string)$roots['admin'], '/\\') . '/controller/extension/module/easychat.php',
            rtrim((string)$roots['admin'], '/\\') . '/controller/extension/module/easychat/recovery_v3.php',
            rtrim((string)$roots['catalog'], '/\\') . '/controller/extension/module/easychat.php',
        );
        if (!$this->invalidateAdapterPhpFiles($controllers)) {
            return $this->fail('adapter_cleanup_controller_invalidation_failed');
        }

        $directories = array(
            array('admin', 'controller/extension/module/easychat/recovery_v3'),
            array('admin', 'controller/extension/module/easychat'),
            array('catalog', 'controller/extension/module/easychat'),
            array('system', 'library/easychat_adapter_runtime/admin'),
            array('system', 'library/easychat_adapter_runtime/catalog'),
            array('system', 'library/easychat_adapter_runtime/recovery'),
            array('system', 'library/easychat_runtime_publisher'),
        );
        foreach ($directories as $definition) {
            if (!$this->cleanupManagedImplementationDirectory(
                (string)$roots[$definition[0]],
                $definition[0] . '/' . $definition[1],
                $definition[1],
                $expected
            )) {
                return false;
            }
        }

        foreach (array(
            rtrim((string)$roots['admin'], '/\\') . '/controller/extension/module/easychat.php',
            rtrim((string)$roots['admin'], '/\\') . '/controller/extension/module/easychat/recovery_v3.php',
            rtrim((string)$roots['catalog'], '/\\') . '/controller/extension/module/easychat.php',
        ) as $controller) {
            if (!$this->cleanupControllerTemporaryFiles($controller)) {
                return false;
            }
        }

        return true;
    }

    private function cleanupManagedImplementationDirectory($root, $relativeWithRoot, $relativeDirectory, array $expected) {
        $root = rtrim((string)$root, '/\\');
        $directory = $root . '/' . ltrim((string)$relativeDirectory, '/');
        clearstatcache(true, $directory);
        if (!file_exists($directory) && !is_link($directory)) {
            return true;
        }
        $canonicalRoot = $this->safeCall(function () use ($root) {
            return realpath($root);
        }, false);
        $canonicalDirectory = $this->safeCall(function () use ($directory) {
            return realpath($directory);
        }, false);
        if (!is_string($canonicalRoot) || $canonicalRoot === ''
            || !is_string($canonicalDirectory) || $canonicalDirectory === ''
            || !is_dir($directory) || is_link($directory)
            || strpos($canonicalDirectory, rtrim($canonicalRoot, '/\\') . DIRECTORY_SEPARATOR) !== 0
        ) {
            return $this->fail('adapter_cleanup_directory_invalid');
        }

        $entries = $this->safeCall(function () use ($directory) {
            return scandir($directory);
        }, false);
        if (!is_array($entries)) {
            return $this->fail('adapter_cleanup_scan_failed');
        }
        $remove = array();
        foreach ($entries as $name) {
            if ($name === '.' || $name === '..') {
                continue;
            }
            $relativePath = $relativeWithRoot . '/' . $name;
            if (isset($expected[$relativePath])) {
                continue;
            }
            $path = $directory . '/' . $name;
            if (!preg_match('/^00_impl_r[1-9][0-9]*_([a-f0-9]{64})\.php$/', $name, $match)
                || !$this->ownedRegularFile($path)
                || !hash_equals($match[1], (string)$this->hashFile($path))
            ) {
                continue;
            }
            $remove[$path] = (string)$match[1];
        }

        $pointer = $directory . '/zz_current.json';
        $pointerRelative = $relativeWithRoot . '/zz_current.json';
        $pointerHash = '';
        if (!isset($expected[$pointerRelative]) && $this->validImplementationPointer($pointer, $directory)) {
            $pointerHash = (string)$this->hashFile($pointer);
        }
        if ($remove && !$this->invalidateAdapterPhpFiles(array_keys($remove))) {
            return $this->fail('adapter_cleanup_implementation_invalidation_failed');
        }
        foreach ($remove as $path => $hash) {
            clearstatcache(true, $path);
            if (!$this->ownedRegularFile($path)
                || !hash_equals($hash, (string)$this->hashFile($path))
                || !$this->removeFile($path)
            ) {
                return $this->fail('adapter_cleanup_implementation_remove_failed');
            }
        }
        if ($pointerHash !== '') {
            clearstatcache(true, $pointer);
            if (!$this->ownedRegularFile($pointer)
                || !hash_equals($pointerHash, (string)$this->hashFile($pointer))
                || !$this->removeFile($pointer)
            ) {
                return $this->fail('adapter_cleanup_pointer_remove_failed');
            }
        }

        $remaining = $this->safeCall(function () use ($directory) {
            return scandir($directory);
        }, false);
        if (is_array($remaining) && count($remaining) === 2) {
            $this->safeCall(function () use ($directory) {
                return rmdir($directory);
            }, false);
        }
        return true;
    }

    private function validImplementationPointer($pointer, $directory) {
        if (!$this->ownedRegularFile($pointer)) {
            return false;
        }
        $definition = $this->readJson($pointer);
        if (!is_array($definition)
            || (int)($definition['format'] ?? 0) !== 1
            || !is_int($definition['revision'] ?? null)
            || (int)$definition['revision'] < 1
            || !is_string($definition['sha256'] ?? null)
            || !preg_match('/^[a-f0-9]{64}$/', $definition['sha256'])
        ) {
            return false;
        }
        $name = '00_impl_r' . $definition['revision'] . '_' . $definition['sha256'] . '.php';
        $implementation = $directory . '/' . $name;
        return hash_equals($name, (string)($definition['implementation'] ?? ''))
            && $this->ownedRegularFile($implementation)
            && hash_equals($definition['sha256'], (string)$this->hashFile($implementation));
    }

    private function cleanupControllerTemporaryFiles($controller) {
        $matches = $this->safeCall(function () use ($controller) {
            return glob($controller . '.easychat-new-*', GLOB_NOSORT);
        }, false);
        if (!is_array($matches)) {
            return $this->fail('adapter_cleanup_temporary_scan_failed');
        }
        foreach ($matches as $path) {
            $suffix = substr($path, strlen($controller . '.easychat-new-'));
            if (!preg_match('/^[a-f0-9]{16}$/', (string)$suffix)
                || !$this->ownedRegularFile($path)
            ) {
                continue;
            }
            $source = $this->safeCall(function () use ($path) {
                return file_get_contents($path, false, null, 0, 65537);
            }, false);
            if (!is_string($source) || strlen($source) > 65536) {
                continue;
            }
             
             
             
             
            if ($source !== ''
                && strpos($source, "define('EASYCHAT_OPENCART_STABLE_CONTROLLER_LOADER', 1)") === false
            ) {
                continue;
            }
            $hash = (string)$this->hashFile($path);
            if ($hash === '' || !hash_equals($hash, (string)$this->hashFile($path)) || !$this->removeFile($path)) {
                return $this->fail('adapter_cleanup_temporary_remove_failed');
            }
        }
        return true;
    }

    private function ownedRegularFile($path) {
        clearstatcache(true, $path);
        if (!is_file($path) || is_link($path)) {
            return false;
        }
        $stat = $this->safeCall(function () use ($path) {
            return lstat($path);
        }, false);
        return is_array($stat)
            && (($stat['mode'] ?? 0) & 0170000) === 0100000
            && (int)($stat['nlink'] ?? 0) === 1;
    }

    public function reconcileOpenCartInstallHistory($db, $tablePrefix, array $roots, $expectedVersion, $expectationFile, $bundleManifestFile) {
        $this->lastError = '';
        $tablePrefix = (string)$tablePrefix;
        $expectedVersion = trim((string)$expectedVersion);
        if (!is_object($db) || !method_exists($db, 'query')
            || !preg_match('/^[a-zA-Z0-9_]{0,64}$/', $tablePrefix)
            || $expectedVersion === ''
        ) {
            return $this->fail('install_history_arguments_invalid');
        }
        $expectation = $this->readJson($expectationFile);
        $bundleManifest = $this->readJson($bundleManifestFile);
        if (!is_array($expectation) || !is_array($bundleManifest)
            || !hash_equals($expectedVersion, (string)($expectation['version'] ?? ''))
            || !hash_equals($expectedVersion, (string)($bundleManifest['version'] ?? ''))
            || !hash_equals(self::PUBLISH_REVISION, (string)($expectation['publish_revision'] ?? ''))
            || !hash_equals(self::PUBLISH_REVISION, (string)($bundleManifest['publish_revision'] ?? ''))
            || !is_array($expectation['files'] ?? null)
            || !is_array($bundleManifest['files'] ?? null)
        ) {
            return $this->fail('install_history_expectation_invalid');
        }

        $expectedPaths = array();
        foreach (array_keys($expectation['files']) as $path) {
            $path = $this->normalizeRelativePath($path);
            if ($path === '') {
                return $this->fail('install_history_expectation_invalid');
            }
            $expectedPaths[$path] = true;
        }
        $migrationFiles = $this->normalizeMigrationExpectation($expectation, $expectedPaths);
        if ($migrationFiles === false) {
            return false;
        }
        $migrationPaths = array_fill_keys(array_keys($migrationFiles), true);
        foreach (array_keys($bundleManifest['files']) as $path) {
            $path = $this->normalizeRelativePath($path);
            if ($path === '') {
                return $this->fail('install_history_expectation_invalid');
            }
            $expectedPaths['system/library/easychat_bundle_r3/' . $path] = true;
        }
        $expectedPaths['system/library/easychat_bundle_r3/easychat-bundle-manifest.json'] = true;
        $expectedPaths['system/library/easychat_adapter_expectation.json'] = true;
        ksort($expectedPaths, SORT_STRING);
        $expectedAncestors = array();
        foreach (array_keys($expectedPaths) as $path) {
            $directory = dirname($path);
            while ($directory !== '.' && $directory !== '' && $directory !== DIRECTORY_SEPARATOR) {
                $expectedAncestors[str_replace('\\', '/', $directory)] = true;
                $directory = dirname($directory);
            }
        }
        $archivePaths = $expectedPaths + $migrationPaths;
        $archiveAncestors = $expectedAncestors;
        foreach (array_keys($migrationPaths) as $path) {
            $directory = dirname($path);
            while ($directory !== '.' && $directory !== '' && $directory !== DIRECTORY_SEPARATOR) {
                $archiveAncestors[str_replace('\\', '/', $directory)] = true;
                $directory = dirname($directory);
            }
        }

        $aliases = array();
        foreach (array('admin', 'catalog', 'system') as $logical) {
            $root = isset($roots[$logical]) ? rtrim((string)$roots[$logical], '/\\') : '';
            if ($root === '' || !is_dir($root)) {
                return $this->fail('install_history_root_invalid');
            }
            $aliases[basename($root)] = $logical;
            $aliases[$logical] = $logical;
        }

        $modificationTable = '`' . $tablePrefix . 'modification`';
        $installTable = '`' . $tablePrefix . 'extension_install`';
        $pathTable = '`' . $tablePrefix . 'extension_path`';
        $locked = false;
        try {
            if ($this->queryRows($db, 'LOCK TABLES ' . $modificationTable . ' READ, ' . $installTable . ' READ, ' . $pathTable . ' WRITE') === false) {
                return $this->fail('install_history_lock_failed');
            }
            $locked = true;
            $modifications = $this->queryRows(
                $db,
                'SELECT `modification_id`, `extension_install_id`, `name`, `code`, `author`, `version`, `link` FROM '
                . $modificationTable . " WHERE `code` = 'easychat'"
            );
            if ($modifications === false) {
                return $this->fail('install_history_modification_read_failed');
            }
            $currentIds = array();
            foreach ($modifications as $row) {
                $id = (int)($row['extension_install_id'] ?? 0);
                if ($id > 0
                    && hash_equals('easychat', (string)($row['code'] ?? ''))
                    && in_array((string)($row['name'] ?? ''), array(
                        'EasyChat for OpenCart 3',
                        'EasyChat for OpenCart 2.3 and 3',
                    ), true)
                    && hash_equals('Wellsoft', (string)($row['author'] ?? ''))
                    && hash_equals('https://github.com/murzein/StandaloneChatServer', (string)($row['link'] ?? ''))
                    && hash_equals($expectedVersion, (string)($row['version'] ?? ''))
                ) {
                    $currentIds[$id] = true;
                }
            }
            if (count($currentIds) !== 1) {
                return $this->fail('install_history_current_owner_ambiguous');
            }
            $currentId = (int)array_key_first($currentIds);
            $installRows = $this->queryRows(
                $db,
                'SELECT `extension_install_id` FROM ' . $installTable . ' WHERE `extension_install_id` = ' . $currentId
            );
            if ($installRows === false || count($installRows) !== 1) {
                return $this->fail('install_history_current_install_missing');
            }

            $candidateRows = $this->queryRows(
                $db,
                'SELECT `extension_path_id`, `extension_install_id`, `path` FROM ' . $pathTable
                . " WHERE `path` LIKE '%easychat%' OR `extension_install_id` = " . $currentId
                . ' ORDER BY `extension_install_id`, `extension_path_id`'
            );
            if ($candidateRows === false) {
                return $this->fail('install_history_paths_read_failed');
            }
            $sentinelIds = array($currentId => true);
            foreach ($candidateRows as $row) {
                $normalized = $this->normalizeInstallHistoryPath($row['path'] ?? '', $aliases);
                $id = (int)($row['extension_install_id'] ?? 0);
                if ($id > 0 && $normalized !== '' && $this->isEasyChatHistorySentinel($normalized, $expectedPaths)) {
                    $sentinelIds[$id] = true;
                }
            }
            $candidateIds = array_keys($sentinelIds);
            sort($candidateIds, SORT_NUMERIC);
            $candidateModifications = $this->queryRows(
                $db,
                'SELECT `modification_id`, `extension_install_id`, `name`, `code`, `author`, `version`, `link` FROM '
                . $modificationTable . ' WHERE `extension_install_id` IN ('
                . implode(',', array_map('intval', $candidateIds)) . ')'
            );
            if ($candidateModifications === false) {
                return $this->fail('install_history_modification_refs_read_failed');
            }
            $modificationsByInstall = array();
            foreach ($candidateModifications as $row) {
                $id = (int)($row['extension_install_id'] ?? 0);
                if ($id > 0) {
                    $modificationsByInstall[$id][] = $row;
                }
            }
            foreach ($candidateIds as $id) {
                $id = (int)$id;
                $references = $modificationsByInstall[$id] ?? array();
                 
                 
                 
                 
                 
                if ($id === $currentId && count($references) !== 1) {
                    return $this->fail('install_history_modification_refs_ambiguous');
                }
                foreach ($references as $row) {
                    if (!hash_equals('easychat', (string)($row['code'] ?? ''))
                        || !hash_equals('Wellsoft', (string)($row['author'] ?? ''))
                        || !hash_equals('https://github.com/murzein/StandaloneChatServer', (string)($row['link'] ?? ''))
                        || stripos((string)($row['name'] ?? ''), 'EasyChat') === false
                    ) {
                        return $this->fail('install_history_foreign_modification_reference');
                    }
                }
            }
            $candidateInstallRows = $this->queryRows(
                $db,
                'SELECT `extension_install_id` FROM ' . $installTable . ' WHERE `extension_install_id` IN ('
                . implode(',', array_map('intval', $candidateIds)) . ')'
            );
            if ($candidateInstallRows === false) {
                return $this->fail('install_history_install_read_failed');
            }
            $knownInstallIds = array();
            foreach ($candidateInstallRows as $row) {
                $knownInstallIds[(int)($row['extension_install_id'] ?? 0)] = true;
            }
            foreach ($candidateIds as $id) {
                if (!isset($knownInstallIds[(int)$id])) {
                    return $this->fail('install_history_owner_missing');
                }
            }
            $allRows = $this->queryRows(
                $db,
                'SELECT `extension_path_id`, `extension_install_id`, `path` FROM ' . $pathTable
                . ' WHERE `extension_install_id` IN (' . implode(',', array_map('intval', $candidateIds)) . ')'
                . ' ORDER BY `extension_install_id`, `extension_path_id`'
            );
            if ($allRows === false) {
                return $this->fail('install_history_paths_read_failed');
            }
            $rowsById = array();
            foreach ($allRows as $row) {
                $id = (int)($row['extension_install_id'] ?? 0);
                $pathId = (int)($row['extension_path_id'] ?? 0);
                $path = $this->normalizeInstallHistoryPath($row['path'] ?? '', $aliases);
                if ($id <= 0 || $pathId <= 0 || $path === '') {
                    return $this->fail('install_history_path_invalid');
                }
                $rowsById[$id][] = array('id' => $pathId, 'path' => $path);
            }
            $currentPathSet = array();
            foreach ($rowsById[$currentId] ?? array() as $row) {
                $currentPathSet[$row['path']] = true;
                if (!isset($archivePaths[$row['path']])
                    && !isset($archiveAncestors[$row['path']])
                ) {
                    return $this->fail('install_history_current_paths_ambiguous');
                }
            }
            foreach ($expectedPaths as $path => $_) {
                if (!isset($currentPathSet[$path])) {
                    return $this->fail('install_history_current_paths_incomplete');
                }
            }

            $deleteIds = array();
             
             
             
             
            foreach ($rowsById[$currentId] ?? array() as $row) {
                if (!isset($expectedPaths[$row['path']])
                    && !isset($expectedAncestors[$row['path']])
                ) {
                    $deleteIds[] = (int)$row['id'];
                }
            }
            foreach ($candidateIds as $id) {
                $id = (int)$id;
                if ($id === $currentId) {
                    continue;
                }
                if ($id > $currentId) {
                    return $this->fail('install_history_future_owner_present');
                }
                $rows = $rowsById[$id] ?? array();
                if (!$rows) {
                    continue;
                }
                $hasSentinel = false;
                foreach ($rows as $row) {
                    if (!$this->isAllowlistedEasyChatHistoryPath($row['path'], $expectedPaths, $expectedAncestors)) {
                        return $this->fail('install_history_superseded_paths_ambiguous');
                    }
                    $hasSentinel = $hasSentinel || $this->isEasyChatHistorySentinel($row['path'], $expectedPaths);
                    $deleteIds[] = (int)$row['id'];
                }
                if (!$hasSentinel) {
                    return $this->fail('install_history_superseded_owner_ambiguous');
                }
            }
            $deleteIds = array_values(array_unique(array_filter($deleteIds)));
            sort($deleteIds, SORT_NUMERIC);
            if ($deleteIds) {
                $deleteResult = $this->queryRows(
                    $db,
                    'DELETE FROM ' . $pathTable . ' WHERE `extension_path_id` IN ('
                    . implode(',', array_map('intval', $deleteIds)) . ')'
                );
                if ($deleteResult === false) {
                    return $this->fail('install_history_cleanup_failed');
                }
                $remaining = $this->queryRows(
                    $db,
                    'SELECT `extension_path_id` FROM ' . $pathTable . ' WHERE `extension_path_id` IN ('
                    . implode(',', array_map('intval', $deleteIds)) . ')'
                );
                if ($remaining === false || count($remaining) !== 0) {
                    return $this->fail('install_history_cleanup_readback_failed');
                }
            }
            return true;
        } finally {
            if ($locked) {
                $this->queryRows($db, 'UNLOCK TABLES');
            }
        }
    }

    public function invalidateAdapterPhpFiles(array $paths) {
        foreach (array_values(array_unique($paths)) as $path) {
            $path = (string)$path;
            if ($path === '' || !is_file($path)) {
                continue;
            }
            if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'php'
                || !$this->invalidatePhp($path)
            ) {
                return false;
            }
        }
        return true;
    }

    public function isKnownLegacyAdapterFile($path, $relativePath) {
        $relativePath = ltrim(str_replace('\\', '/', (string)$relativePath), '/');
        $fingerprints = $this->legacyAdapterFingerprints();
        if (!isset($fingerprints[$relativePath])) {
            return false;
        }

        $path = (string)$path;
        clearstatcache(true, $path);
        if (!is_file($path) || is_link($path)) {
            return false;
        }
        $stat = $this->safeCall(function () use ($path) {
            return lstat($path);
        }, false);
        if (!is_array($stat) || (int)($stat['nlink'] ?? 0) !== 1) {
            return false;
        }
        $source = $this->safeCall(function () use ($path) {
            return file_get_contents($path, false, null, 0, 65537);
        }, false);
        if (!is_string($source) || strlen($source) > 65536) {
            return false;
        }

         
         
         
        $canonical = str_replace(array("\r\n", "\r"), "\n", $source);
        $actual = hash('sha256', $canonical);
        foreach ($fingerprints[$relativePath] as $expected) {
            if (hash_equals($expected, $actual)) {
                return true;
            }
        }
        return false;
    }

    private function legacyAdapterFingerprints() {
         
         
         
        return array(
            'admin/controller/module/easychat.php' => array(
                '1970e676d72ec522384d2d80cf666218636d337f5b24d9fed9f1295940e0dcca',
                '43b4c44783857f8209d49c57f3915d1a944166e16636dd185d955f76fc9d0aa6',
                '555e18e4c55b73b402ab23be25fac1960f68ddac3675431bf1378a22875eb88f',
                '55f936c9ee2711c50c4cb59e7ce32754945c766fa4efa138f3a7992be152d19e',
                '59de321d85e12286000122fea1fbe404d1b3f6168d91fcb576e37c5445a6e652',
                '59e0dcc7accfa236e50080d27cffb2530223cef921cdca26450bdf392c1896ad',
                '86d731992790a803029841b6feea6c4f9617e6285f82eb861c31372b6838e752',
                'ac32bc7c72d646729266b1fbd322a4ac6011e4b7bb97e7cd31b0171d65ab029e',
                'adfbebcef3c6d40a02b99b5c5e49b2d019bddb114e0b8e42eef62fa9a6b428c2',
                'e1e32814241294e4749d9cf21fcdc06546283816331a68bafbe10034771dde4b',
            ),
            'admin/language/en-gb/module/easychat.php' => array(
                '8d1348fb5c4e994dbd76c5e9f6edce56e318b082a7f4437ea15bdd3234f3a001',
            ),
            'admin/language/es-es/module/easychat.php' => array(
                '60afc23bd6a50d346d930f8cf621d30a96b4be9fdb70c0c164813755949a5bde',
            ),
            'admin/language/kk-kz/module/easychat.php' => array(
                '0810930ab0b5f35de0c4c6d913083b10d571ae3b677458bb69946e870b2b2ea2',
            ),
            'admin/language/ru-ru/module/easychat.php' => array(
                '5c65fde8797460f3d6c76dbcb16e522f031b49b443cedc99cbca9fb6c85165c7',
                'ad85ed54c9c90ba6ac87608e2af24097bde25189fe37e208c3fdcc00e3578aa5',
            ),
            'admin/language/uk-ua/module/easychat.php' => array(
                '97a7b4678fc2f19f0a8302c90fad812c663fc22e36420296aa2ce5f309329795',
            ),
            'admin/language/uz-uz/module/easychat.php' => array(
                '64b669ff6053714c60d2a16de2e91f1a913e1a0b3f48c164257631a46ad0421d',
            ),
            'admin/view/template/module/easychat.tpl' => array(
                '1673aa55e0751d8c90c07e15e251d9a192d134f71c3056db5653ba89a7467aa6',
                '8ecd1a9eda957229af3356dcd71c7b19c5e7f8715c678e3d110fc96eca00c79f',
                'cee32b32056dd19b8fe866b18a15f3228f2a91c1977383cd3491c17437ed556f',
            ),
            'catalog/controller/module/easychat.php' => array(
                '2e4e7ce02f2d4ca1b392ae4fca0086ad19d070735dca5f570eb666a0a8f2c64b',
                '501a7ed1376c46a7a0279893be5dd9f2de7cd50828fcf521d5adaa2c51a9571d',
                '776ba64b26f6a5bfcdee2a235a0bf4a5c21f9df4700cfb5249d840470728e3d0',
                'd44ae5f149816b55f4de6cb34b03d57b89eabd04c51673aa7c73c4b25f456d25',
            ),
            'catalog/view/theme/default/template/module/easychat.tpl' => array(
                '8c5f68f384a74d765e4a7db26d3e975a159f5cb3d35d21f553c916c17e41112d',
                'cfe4aad4ab634af12b281d14f32a5d0363c5e7682785c8c4975ed2b1233f1cd4',
            ),
        );
    }

    protected function checkpoint($name) {
         
    }

    private function ensureLegacyDrainGate($runtime, $expectedVersion) {
        if ($this->runtimeHasManagedRevision($runtime)) {
            return 0;
        }
        $statePath = $this->legacyDrainStatePath($runtime);
        $state = $this->readJson($statePath);
        if (is_array($state) && !$this->privateRegularFile($statePath)) {
            return false;
        }
        if ($state === false) {
            if (file_exists($statePath) || is_link($statePath)) {
                return false;
            }
            if (!is_file($runtime . '/index.php')) {
                return 0;
            }
            $state = $this->createLegacyDrainBackup($runtime, $expectedVersion);
            if ($state === false) {
                return false;
            }
        }
        if (!$this->validLegacyDrainState($state, $expectedVersion)) {
            return false;
        }
        $this->legacyDrainResumeToken = (string)$state['resume_token'];
        if ((string)$state['phase'] === 'prepared') {
            if (!$this->restoreLegacyDrainFiles($runtime, $state)
                || !$this->applyLegacyDrainGates($runtime)
            ) {
                $this->restoreLegacyDrainFiles($runtime, $state);
                return false;
            }
            $state['phase'] = 'draining';
            $state['ready_at'] = time() + self::LEGACY_DRAIN_SECONDS;
            if (!$this->writeJsonAtomic($statePath, $state, 0600)) {
                $this->restoreLegacyDrainFiles($runtime, $state);
                return false;
            }
        }
        if ((string)$state['phase'] !== 'draining'
            || !$this->legacyDrainBackupMatches($runtime, $state)
        ) {
            return false;
        }
        if (!$this->legacyDrainGatesMatch($runtime)) {
             
             
             
             
             
            if (!$this->restoreLegacyDrainFiles($runtime, $state)
                || !$this->applyLegacyDrainGates($runtime)
            ) {
                $this->restoreLegacyDrainFiles($runtime, $state);
                return false;
            }
            $state['ready_at'] = time() + self::LEGACY_DRAIN_SECONDS;
            if (!$this->writeJsonAtomic($statePath, $state, 0600)
                || !$this->legacyDrainBackupMatches($runtime, $state)
                || !$this->legacyDrainGatesMatch($runtime)
            ) {
                $this->restoreLegacyDrainFiles($runtime, $state);
                return false;
            }
        }
        return (int)$state['ready_at'];
    }

    private function runtimeHasManagedRevision($runtime) {
        foreach (array(
            $this->durableManagedMarkerPath($runtime),
            $this->publishedManifestPath($runtime),
        ) as $path) {
            $metadata = $this->readJson($path);
            if (is_array($metadata)
                && (int)($metadata['publish_revision'] ?? 0) >= 3
            ) {
                return true;
            }
        }
        return false;
    }

    private function createLegacyDrainBackup($runtime, $expectedVersion) {
        try {
            $transactionId = bin2hex(random_bytes(16));
            $resumeToken = bin2hex(random_bytes(32));
        } catch (\Throwable $e) {
            return false;
        }
        $transaction = $this->legacyDrainRootPath($runtime) . '/' . $transactionId;
        if (!$this->createDirectory($transaction, 0700)
            || !$this->hardenDirectory($transaction, 0700, 0077)
        ) {
            return false;
        }
        $files = array();
        foreach ($this->legacyDrainPaths() as $relativePath) {
            $source = rtrim($runtime, '/\\') . '/' . $relativePath;
            if (!file_exists($source) && !is_link($source)) {
                $files[$relativePath] = array(
                    'present' => false,
                    'sha256' => str_repeat('0', 64),
                    'size' => 0,
                    'mode' => 0,
                );
                continue;
            }
            if (!is_file($source) || is_link($source)) {
                $this->removeTree($transaction);
                return false;
            }
            $hash = $this->hashFile($source);
            $size = $this->safeCall(function () use ($source) {
                return filesize($source);
            }, false);
            $permissions = $this->safeCall(function () use ($source) {
                return fileperms($source);
            }, false);
            $entry = array(
                'present' => true,
                'sha256' => $hash,
                'size' => (int)$size,
                'mode' => (int)($permissions & 0777),
            );
            $manifestEntry = array(
                'sha256' => $entry['sha256'],
                'size' => $entry['size'],
                'mode' => $entry['mode'],
            );
            $blob = $transaction . '/' . $entry['sha256'] . '.blob';
            if ($hash === false || $size === false || $permissions === false
                || !$this->validManifestEntry($manifestEntry)
                || (!is_file($blob) && !$this->copyFileAtomic($source, $blob, $manifestEntry, false))
            ) {
                $this->removeTree($transaction);
                return false;
            }
            $files[$relativePath] = $entry;
        }
        $state = array(
            'format' => self::MANIFEST_FORMAT,
            'publish_revision' => self::PUBLISH_REVISION,
            'expected_version' => (string)$expectedVersion,
            'transaction' => $transactionId,
            'resume_token' => $resumeToken,
            'phase' => 'prepared',
            'ready_at' => 0,
            'files' => $files,
        );
        if (!$this->writeJsonAtomic($this->legacyDrainStatePath($runtime), $state, 0600)) {
            $this->removeTree($transaction);
            return false;
        }
        return $state;
    }

    private function validLegacyDrainState($state, $expectedVersion) {
        if (!is_array($state)
            || (int)($state['format'] ?? 0) !== self::MANIFEST_FORMAT
            || !hash_equals(self::PUBLISH_REVISION, (string)($state['publish_revision'] ?? ''))
            || !hash_equals((string)$expectedVersion, (string)($state['expected_version'] ?? ''))
            || !preg_match('/^[a-f0-9]{32}$/', (string)($state['transaction'] ?? ''))
            || !preg_match('/^[a-f0-9]{64}$/', (string)($state['resume_token'] ?? ''))
            || !in_array((string)($state['phase'] ?? ''), array('prepared', 'draining'), true)
            || !is_int($state['ready_at'] ?? null)
            || !is_array($state['files'] ?? null)
            || array_keys($state['files']) !== $this->legacyDrainPaths()
        ) {
            return false;
        }
        foreach ($state['files'] as $entry) {
            if (!is_array($entry)
                || !is_bool($entry['present'] ?? null)
                || !preg_match('/^[a-f0-9]{64}$/', (string)($entry['sha256'] ?? ''))
                || !is_int($entry['size'] ?? null)
                || !is_int($entry['mode'] ?? null)
                || $entry['size'] < 0
                || $entry['mode'] < 0
                || $entry['mode'] > 0777
            ) {
                return false;
            }
        }
        return true;
    }

    private function legacyDrainPaths() {
        return array(
            'core/runtime_reader_lock.php',
            'cron.php',
            'cli/max_polling.php',
            'cli/max_qr_auth_worker.php',
        );
    }

    private function legacyDrainGateEntries() {
        $cliGate = "<?php\nif (PHP_SAPI === 'cli') { fwrite(STDERR, 'EasyChat update in progress.' . PHP_EOL); exit(75); }\nhttp_response_code(503);\nheader('Retry-After: 30');\nheader('Cache-Control: no-store, no-cache, must-revalidate');\nexit('EasyChat is completing a secure update. Retry shortly.');\n";
         
         
         
         
         
        $readerGate = str_replace('__EASYCHAT_PUBLISH_REVISION__', self::PUBLISH_REVISION, <<<'PHP'
<?php
$easychat_drain_runtime = dirname(__DIR__);
$easychat_drain_state_path = $easychat_drain_runtime . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR . 'easychat-adoption-r3.json';
$easychat_drain_state = null;
$easychat_drain_safe_call = static function ($callback, $fallback = false) {
    set_error_handler(static function () { return true; });
    try {
        return $callback();
    } catch (\Throwable $error) {
        return $fallback;
    } finally {
        restore_error_handler();
    }
};
$easychat_drain_handle = (!is_link($easychat_drain_state_path) && is_file($easychat_drain_state_path))
    ? $easychat_drain_safe_call(static function () use ($easychat_drain_state_path) {
        return fopen($easychat_drain_state_path, 'rb');
    })
    : false;
if (is_resource($easychat_drain_handle)) {
    $easychat_drain_path_stat = $easychat_drain_safe_call(static function () use ($easychat_drain_state_path) {
        return lstat($easychat_drain_state_path);
    });
    $easychat_drain_handle_stat = $easychat_drain_safe_call(static function () use ($easychat_drain_handle) {
        return fstat($easychat_drain_handle);
    });
    $easychat_drain_private = is_array($easychat_drain_path_stat)
        && is_array($easychat_drain_handle_stat)
        && (($easychat_drain_path_stat['mode'] ?? 0) & 0170000) === 0100000
        && (($easychat_drain_handle_stat['mode'] ?? 0) & 0170000) === 0100000
        && (($easychat_drain_path_stat['mode'] ?? 0) & 0077) === 0
        && (($easychat_drain_handle_stat['mode'] ?? 0) & 0077) === 0
        && (int)($easychat_drain_path_stat['nlink'] ?? 0) === 1
        && (int)($easychat_drain_handle_stat['nlink'] ?? 0) === 1
        && ($easychat_drain_path_stat['dev'] ?? null) === ($easychat_drain_handle_stat['dev'] ?? null)
        && ($easychat_drain_path_stat['ino'] ?? null) === ($easychat_drain_handle_stat['ino'] ?? null);
    $easychat_drain_raw = $easychat_drain_private
        ? $easychat_drain_safe_call(static function () use ($easychat_drain_handle) {
            return stream_get_contents($easychat_drain_handle, 8193);
        })
        : false;
    fclose($easychat_drain_handle);
    if (is_string($easychat_drain_raw) && strlen($easychat_drain_raw) <= 8192) {
        $easychat_drain_state = json_decode($easychat_drain_raw, true);
    }
}
$easychat_drain_cookie = filter_input(INPUT_COOKIE, 'easychat_update_resume', FILTER_UNSAFE_RAW);
$easychat_drain_token = is_string($easychat_drain_cookie) ? $easychat_drain_cookie : '';
$easychat_drain_method_input = filter_input(INPUT_SERVER, 'REQUEST_METHOD', FILTER_UNSAFE_RAW);
$easychat_drain_route_input = filter_input(INPUT_GET, 'route', FILTER_UNSAFE_RAW);
$easychat_drain_has_post = filter_has_var(INPUT_POST, 'csrf_token');
$easychat_drain_method = is_string($easychat_drain_method_input) && $easychat_drain_method_input !== ''
    ? $easychat_drain_method_input
    : ($easychat_drain_has_post ? 'POST' : 'GET');
$easychat_drain_route = is_string($easychat_drain_route_input) ? $easychat_drain_route_input : '';
$easychat_drain_ready = is_array($easychat_drain_state)
    && (int)($easychat_drain_state['ready_at'] ?? 0) <= time();
$easychat_drain_route_allowed = ($easychat_drain_method === 'POST' && $easychat_drain_route === 'update/install')
    || ($easychat_drain_ready && $easychat_drain_method === 'POST' && in_array(
        $easychat_drain_route,
        array('update/status', 'update/operationStatus', 'update/advance'),
        true
    ))
    || ($easychat_drain_ready && $easychat_drain_method === 'GET' && $easychat_drain_route === 'update');
$easychat_drain_resume_allowed = PHP_SAPI !== 'cli'
    && $easychat_drain_route_allowed
    && is_array($easychat_drain_state)
    && (int)($easychat_drain_state['format'] ?? 0) === 1
    && (string)($easychat_drain_state['publish_revision'] ?? '') === '__EASYCHAT_PUBLISH_REVISION__'
    && (string)($easychat_drain_state['phase'] ?? '') === 'draining'
    && preg_match('/^[a-f0-9]{64}$/D', (string)($easychat_drain_state['resume_token'] ?? '')) === 1
    && preg_match('/^[a-f0-9]{64}$/D', $easychat_drain_token) === 1
    && hash_equals((string)$easychat_drain_state['resume_token'], $easychat_drain_token);
if ($easychat_drain_resume_allowed) {
    // index.php ожидает reader API сразу после require_once. На узко
    // разрешенном resume-route текущий запрос становится writer-ом сам.
    function easychat_runtime_reader_lock_acquire($runtime_root, $web_response) { return null; }
    function easychat_runtime_reader_lock_release() { return true; }
    function easychat_runtime_reader_lock_checkpoint($before_exit = null) { return true; }
    unset(
        $easychat_drain_raw,
        $easychat_drain_path_stat,
        $easychat_drain_handle_stat,
        $easychat_drain_private,
        $easychat_drain_state,
        $easychat_drain_cookie,
        $easychat_drain_token,
        $easychat_drain_ready,
        $easychat_drain_route_allowed,
        $easychat_drain_resume_allowed,
        $easychat_drain_safe_call,
        $easychat_drain_runtime,
        $easychat_drain_state_path,
        $easychat_drain_method_input,
        $easychat_drain_has_post,
        $easychat_drain_route_input,
        $easychat_drain_method,
        $easychat_drain_route
    );
    return;
}
if (PHP_SAPI === 'cli') {
    fwrite(STDERR, 'EasyChat update in progress.' . PHP_EOL);
    exit(75);
}
$easychat_drain_retry_after = 30;
if (is_array($easychat_drain_state) && (int)($easychat_drain_state['ready_at'] ?? 0) > time()) {
    $easychat_drain_retry_after = max(1, min(300, (int)$easychat_drain_state['ready_at'] - time()));
}
http_response_code(503);
header('Retry-After: ' . $easychat_drain_retry_after);
header('Cache-Control: no-store, no-cache, must-revalidate');
exit('EasyChat is completing a secure update. Retry shortly.');
PHP
        );
        $entries = array();
        foreach ($this->legacyDrainPaths() as $relativePath) {
            $contents = $relativePath === 'core/runtime_reader_lock.php' ? $readerGate : $cliGate;
            $entries[$relativePath] = array(
                'contents' => $contents,
                'sha256' => hash('sha256', $contents),
                'size' => strlen($contents),
                'mode' => 0644,
            );
        }
        return $entries;
    }

    private function applyLegacyDrainGates($runtime) {
        foreach ($this->legacyDrainGateEntries() as $relativePath => $entry) {
            $path = rtrim($runtime, '/\\') . '/' . $relativePath;
            if (!$this->writeFileAtomic($path, $entry['contents'], $entry['mode'])
                || (strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'php'
                    && !$this->invalidatePhp($path))
                || !$this->fileMatchesEntry($path, $entry)
            ) {
                return false;
            }
        }
        return true;
    }

    private function legacyDrainGatesMatch($runtime) {
        foreach ($this->legacyDrainGateEntries() as $relativePath => $entry) {
            if (!$this->fileMatchesEntry(rtrim($runtime, '/\\') . '/' . $relativePath, $entry)) {
                return false;
            }
        }
        return true;
    }

    private function restoreLegacyDrainFiles($runtime, array $state) {
        $transaction = $this->legacyDrainRootPath($runtime) . '/' . $state['transaction'];
        if (!is_dir($transaction) || is_link($transaction)) {
            return false;
        }
        $restored = true;
        foreach ($state['files'] as $relativePath => $entry) {
            $destination = rtrim($runtime, '/\\') . '/' . $relativePath;
            if (!$entry['present']) {
                if (!$this->removeRuntimeFile($destination)) {
                    $restored = false;
                }
                continue;
            }
            $manifestEntry = array(
                'sha256' => $entry['sha256'],
                'size' => $entry['size'],
                'mode' => $entry['mode'],
            );
            $blob = $transaction . '/' . $entry['sha256'] . '.blob';
            if (!$this->fileMatchesEntry($blob, $manifestEntry)
                || !$this->copyFileAtomic($blob, $destination, $manifestEntry, true)
            ) {
                $restored = false;
            }
        }
        return $restored;
    }

    private function legacyDrainBackupMatches($runtime, array $state) {
        $transaction = $this->legacyDrainRootPath($runtime) . '/' . $state['transaction'];
        if (!is_dir($transaction) || is_link($transaction)) {
            return false;
        }
        foreach ($state['files'] as $entry) {
            if (!$entry['present']) {
                continue;
            }
            $manifestEntry = array(
                'sha256' => $entry['sha256'],
                'size' => $entry['size'],
                'mode' => $entry['mode'],
            );
            if (!$this->fileMatchesEntry($transaction . '/' . $entry['sha256'] . '.blob', $manifestEntry)) {
                return false;
            }
        }
        return true;
    }

    private function restoreAndClearLegacyDrain($runtime, $expectedVersion) {
        $state = $this->readJson($this->legacyDrainStatePath($runtime));
        return $this->validLegacyDrainState($state, $expectedVersion)
            && $this->legacyDrainBackupMatches($runtime, $state)
            && $this->restoreLegacyDrainFiles($runtime, $state)
            && $this->clearLegacyDrainMetadata($runtime);
    }

    private function privateRegularFile($path) {
        clearstatcache(true, $path);
        if (!is_file($path) || is_link($path)) {
            return false;
        }
        $stat = $this->safeCall(function () use ($path) {
            return lstat($path);
        }, false);
        return is_array($stat)
            && (($stat['mode'] ?? 0) & 0170000) === 0100000
            && (DIRECTORY_SEPARATOR === '\\' || (($stat['mode'] ?? 0) & 0077) === 0)
            && (int)($stat['nlink'] ?? 0) === 1;
    }

    private function clearLegacyDrainMetadata($runtime) {
        $statePath = $this->legacyDrainStatePath($runtime);
        if (!file_exists($statePath) && !is_link($statePath)) {
            return true;
        }
        $state = $this->readJson($statePath);
        if (!$this->validLegacyDrainState($state, (string)($state['expected_version'] ?? ''))) {
            return false;
        }
        $transaction = $this->legacyDrainRootPath($runtime) . '/' . $state['transaction'];
        return $this->removeTree($transaction)
            && $this->removeFile($statePath);
    }

    private function legacyDrainStatePath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-adoption-r3.json';
    }

    private function legacyDrainRootPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-adoption-r3';
    }

    private function prepareRuntimeStorage($runtime) {
        if ($runtime === '' || is_link($runtime)) {
            return false;
        }
        if (!is_dir($runtime) && !$this->createDirectory($runtime, 0755)) {
            return false;
        }
        if (!is_dir($runtime) || is_link($runtime) || realpath($runtime) === false) {
            return false;
        }

        $log = $runtime . '/log';
        if (is_link($log)
            || (!is_dir($log) && !$this->createDirectory($log, 0700))
            || !is_dir($log)
            || !$this->hardenDirectory($log, 0770, 0007)
        ) {
            return false;
        }

        $protection = $log . '/.htaccess';
        if (is_link($protection) || is_dir($protection)) {
            return false;
        }
        $content = "Options -Indexes\n"
            . "<IfModule mod_authz_core.c>\n    Require all denied\n</IfModule>\n"
            . "<IfModule !mod_authz_core.c>\n    Deny from all\n</IfModule>\n"
            . "RemoveHandler .php .phtml .phar .blob\n";
        $currentProtection = is_file($protection)
            ? $this->safeCall(function () use ($protection) {
                return file_get_contents($protection);
            }, false)
            : false;
        if (!is_string($currentProtection)
            || (strpos($currentProtection, 'Require all denied') === false
                && strpos($currentProtection, 'Deny from all') === false)
        ) {
            if (!$this->writeFileAtomic($protection, $content, 0640)) {
                return false;
            }
        }
        if (!$this->hardenFile($protection, 0640, 0022)) {
            return false;
        }

        $transactions = $this->transactionsPath($runtime);
        return !is_link($transactions)
            && (is_dir($transactions) || $this->createDirectory($transactions, 0700))
            && $this->hardenDirectory($transactions, 0700, 0077);
    }

    private function ensureExecutionWindow() {
        if (PHP_SAPI === 'cli') {
            return true;
        }
        $requiredSeconds = 90;
        $current = (int)$this->safeCall(function () {
            return ini_get('max_execution_time');
        }, 0);
        if ($current === 0 || $current >= $requiredSeconds) {
            return true;
        }
        $changed = $this->safeCall(function () use ($requiredSeconds) {
            return set_time_limit($requiredSeconds);
        }, false);
        $actual = (int)$this->safeCall(function () {
            return ini_get('max_execution_time');
        }, 0);
        return $changed !== false && ($actual === 0 || $actual >= $requiredSeconds);
    }

    private function acquireLock($runtime, $heartbeat = null) {
        $path = $this->lockPath($runtime);
        if (is_link($path) || is_dir($path)) {
            return false;
        }

        $handle = $this->openPrivateLockHandle($path);
        if (!is_resource($handle)) {
            return false;
        }
        clearstatcache(true, $path);
        $fileStat = $this->safeCall(function () use ($path) {
            return lstat($path);
        }, false);
        $handleStat = $this->safeCall(function () use ($handle) {
            return fstat($handle);
        }, false);
        if (!is_array($fileStat)
            || !is_array($handleStat)
            || (($fileStat['mode'] ?? 0) & 0170000) !== 0100000
            || (DIRECTORY_SEPARATOR !== '\\' && (($fileStat['mode'] ?? 0) & 0077) !== 0)
            || (int)($fileStat['nlink'] ?? 0) !== 1
            || ($fileStat['dev'] ?? null) !== ($handleStat['dev'] ?? null)
            || ($fileStat['ino'] ?? null) !== ($handleStat['ino'] ?? null)
        ) {
            fclose($handle);
            return false;
        }

        $deadline = microtime(true) + ($this->lockWaitMilliseconds / 1000);
        $nextHeartbeat = microtime(true);
        do {
            $locked = $this->safeCall(function () use ($handle) {
                return flock($handle, LOCK_EX | LOCK_NB);
            }, false);
            if ($locked) {
                if (!$this->lockHandleMatchesPath($path, $handle)) {
                    $this->safeCall(function () use ($handle) {
                        return flock($handle, LOCK_UN);
                    }, false);
                    fclose($handle);
                    return false;
                }
                return $handle;
            }
            if (microtime(true) >= $deadline) {
                break;
            }
            if ($heartbeat && microtime(true) >= $nextHeartbeat) {
                if (!call_user_func($heartbeat)) {
                    break;
                }
                $nextHeartbeat = microtime(true) + 1;
            }
            usleep(50000);
        } while (true);

        fclose($handle);
        return false;
    }

    private function lockHandleMatchesPath($path, $handle) {
        clearstatcache(true, $path);
        if (is_link($path)) {
            return false;
        }
        $fileStat = $this->safeCall(function () use ($path) {
            return lstat($path);
        }, false);
        $handleStat = $this->safeCall(function () use ($handle) {
            return fstat($handle);
        }, false);
        return is_array($fileStat)
            && is_array($handleStat)
            && (($fileStat['mode'] ?? 0) & 0170000) === 0100000
            && (DIRECTORY_SEPARATOR === '\\' || (($fileStat['mode'] ?? 0) & 0077) === 0)
            && (int)($fileStat['nlink'] ?? 0) === 1
            && ($fileStat['dev'] ?? null) === ($handleStat['dev'] ?? null)
            && ($fileStat['ino'] ?? null) === ($handleStat['ino'] ?? null);
    }

    private function openPrivateLockHandle($path) {
        if (!file_exists($path) && !is_link($path)) {
            $oldUmask = umask(0077);
            try {
                $created = $this->safeCall(function () use ($path) {
                    return fopen($path, 'x+');
                }, false);
            } finally {
                umask($oldUmask);
            }
            if (is_resource($created)) {
                return $created;
            }
        }
        if (is_link($path) || is_dir($path)) {
            return false;
        }
        $existing = $this->safeCall(function () use ($path) {
            return fopen($path, 'r+');
        }, false);
        if (!is_resource($existing)) {
            return false;
        }

         
         
         
         
         
        if (!$this->hardenLegacyLockHandle($path, $existing)) {
            fclose($existing);
            return false;
        }
        return $existing;
    }

    private function hardenLegacyLockHandle($path, $handle) {
        if (!$this->lockHandleIdentityMatchesPath($path, $handle)) {
            return false;
        }
        $handleStat = $this->safeCall(function () use ($handle) {
            return fstat($handle);
        }, false);
        if (!is_array($handleStat)) {
            return false;
        }
        if (DIRECTORY_SEPARATOR === '\\' || (($handleStat['mode'] ?? 0) & 0077) === 0) {
            return true;
        }

        $locked = $this->safeCall(function () use ($handle) {
            return flock($handle, LOCK_EX | LOCK_NB);
        }, false);
        if (!$locked) {
            return false;
        }
        try {
            if (!$this->lockHandleIdentityMatchesPath($path, $handle)) {
                return false;
            }
            $hardened = $this->safeCall(function () use ($path) {
                return chmod($path, 0600);
            }, false);
            clearstatcache(true, $path);
            return $hardened !== false && $this->lockHandleMatchesPath($path, $handle);
        } finally {
            $this->safeCall(function () use ($handle) {
                return flock($handle, LOCK_UN);
            }, false);
        }
    }

    private function lockHandleIdentityMatchesPath($path, $handle) {
        clearstatcache(true, $path);
        if (is_link($path)) {
            return false;
        }
        $fileStat = $this->safeCall(function () use ($path) {
            return lstat($path);
        }, false);
        $handleStat = $this->safeCall(function () use ($handle) {
            return fstat($handle);
        }, false);
        return is_array($fileStat)
            && is_array($handleStat)
            && (($fileStat['mode'] ?? 0) & 0170000) === 0100000
            && (($handleStat['mode'] ?? 0) & 0170000) === 0100000
            && (int)($fileStat['nlink'] ?? 0) === 1
            && (int)($handleStat['nlink'] ?? 0) === 1
            && ($fileStat['dev'] ?? null) === ($handleStat['dev'] ?? null)
            && ($fileStat['ino'] ?? null) === ($handleStat['ino'] ?? null);
    }

    private function releaseLock($handle) {
        $this->safeCall(function () use ($handle) {
            return flock($handle, LOCK_UN);
        }, false);
        fclose($handle);
    }

    private function beginWriterIntent($runtime) {
        try {
            $nonce = bin2hex(random_bytes(16));
        } catch (\Throwable $e) {
            return '';
        }
        $result = $this->withIntentGuard($runtime, function () use ($runtime, $nonce) {
            $path = $this->intentPath($runtime);
            $current = $this->readJson($path);
            if (is_array($current)) {
                if (!$this->validIntent($current)) {
                    return false;
                }
                if ((int)$current['heartbeat_at'] >= time() - self::INTENT_STALE_SECONDS) {
                     
                     
                     
                     
                    if ((!file_exists($this->statePath($runtime))
                            && !is_link($this->statePath($runtime)))
                        || !$this->writerLockAvailableForRecovery($runtime)
                    ) {
                        return false;
                    }
                }
                if (!$this->removeFile($path)) {
                    return false;
                }
            } elseif (file_exists($path) || is_link($path)) {
                return false;
            }

            $now = time();
            $intent = array(
                'format' => self::MANIFEST_FORMAT,
                'nonce' => $nonce,
                'created_at' => $now,
                'heartbeat_at' => $now,
            );
            if (!$this->writeJsonAtomic($path, $intent, 0600)) {
                return false;
            }
            $stored = $this->readJson($path);
            return is_array($stored)
                && $this->validIntent($stored)
                && hash_equals($nonce, (string)$stored['nonce']);
        });
        return $result ? $nonce : '';
    }

    private function writerLockAvailableForRecovery($runtime) {
        $path = $this->lockPath($runtime);
        $handle = $this->openPrivateLockHandle($path);
        if (!is_resource($handle)) {
            return false;
        }
        $locked = $this->safeCall(function () use ($handle) {
            return flock($handle, LOCK_EX | LOCK_NB);
        }, false);
        $valid = $locked && $this->lockHandleMatchesPath($path, $handle);
        if ($locked) {
            $this->safeCall(function () use ($handle) {
                return flock($handle, LOCK_UN);
            }, false);
        }
        fclose($handle);
        return $valid;
    }

    private function refreshWriterIntent($runtime, $nonce) {
        return (bool)$this->withIntentGuard($runtime, function () use ($runtime, $nonce) {
            $path = $this->intentPath($runtime);
            $intent = $this->readJson($path);
            if (!$this->validIntent($intent)
                || !hash_equals((string)$nonce, (string)$intent['nonce'])
            ) {
                return false;
            }
            $intent['heartbeat_at'] = time();
            return $this->writeJsonAtomic($path, $intent, 0600);
        });
    }

    private function writerIntentOwned($runtime, $nonce) {
        $intent = $this->readJson($this->intentPath($runtime));
        return $this->validIntent($intent)
            && hash_equals((string)$nonce, (string)$intent['nonce'])
            && (int)$intent['heartbeat_at'] >= time() - self::INTENT_STALE_SECONDS;
    }

    private function endWriterIntent($runtime, $nonce) {
        return (bool)$this->withIntentGuard($runtime, function () use ($runtime, $nonce) {
            $path = $this->intentPath($runtime);
            $intent = $this->readJson($path);
            if (!is_array($intent)) {
                return !file_exists($path) && !is_link($path);
            }
            if (!$this->validIntent($intent)
                || !hash_equals((string)$nonce, (string)$intent['nonce'])
            ) {
                return false;
            }
            return $this->removeFile($path);
        });
    }

    private function validIntent($intent) {
        return is_array($intent)
            && (int)($intent['format'] ?? 0) === self::MANIFEST_FORMAT
            && preg_match('/^[a-f0-9]{32}$/', (string)($intent['nonce'] ?? ''))
            && is_int($intent['created_at'] ?? null)
            && is_int($intent['heartbeat_at'] ?? null)
            && (int)$intent['created_at'] > 0
            && (int)$intent['heartbeat_at'] >= (int)$intent['created_at'];
    }

    private function writeManagedRuntimeMarkers($runtime) {
        $marker = array(
            'format' => self::MANIFEST_FORMAT,
            'publish_revision' => self::PUBLISH_REVISION,
        );
        foreach (array(
            $this->managedMarkerPath($runtime),
            $this->durableManagedMarkerPath($runtime),
        ) as $path) {
            if (!$this->writeJsonAtomic($path, $marker, 0600)) {
                return false;
            }
            $stored = $this->readJson($path);
            if (!is_array($stored)
                || $stored !== $marker
                || !$this->hardenFile($path, 0600, 0077)
            ) {
                return false;
            }
        }
        return true;
    }

    private function validManagedRuntimeMarker($runtime) {
        $expected = array(
            'format' => self::MANIFEST_FORMAT,
            'publish_revision' => self::PUBLISH_REVISION,
        );
        foreach (array(
            $this->managedMarkerPath($runtime),
            $this->durableManagedMarkerPath($runtime),
        ) as $path) {
            $marker = $this->readJson($path);
            $permissions = $this->safeCall(function () use ($path) {
                return fileperms($path);
            }, false);
            if ($marker !== $expected
                || $permissions === false
                || (DIRECTORY_SEPARATOR !== '\\' && ($permissions & 0077) !== 0)
            ) {
                return false;
            }
        }
        return true;
    }

    private function withIntentGuard($runtime, $callback) {
        $path = $this->intentGuardPath($runtime);
        if (is_link($path) || is_dir($path)) {
            return false;
        }
        $handle = $this->openPrivateLockHandle($path);
        if (!is_resource($handle)) {
            return false;
        }

        $locked = false;
        $deadline = microtime(true) + 1;
        do {
            $locked = $this->safeCall(function () use ($handle) {
                return flock($handle, LOCK_EX | LOCK_NB);
            }, false);
            if ($locked || microtime(true) >= $deadline) {
                break;
            }
            usleep(20000);
        } while (true);

        if (!$locked || !$this->lockHandleMatchesPath($path, $handle)) {
            if ($locked) {
                $this->safeCall(function () use ($handle) {
                    return flock($handle, LOCK_UN);
                }, false);
            }
            fclose($handle);
            return false;
        }

        try {
            return call_user_func($callback);
        } catch (\Throwable $e) {
            return false;
        } finally {
            $this->safeCall(function () use ($handle) {
                return flock($handle, LOCK_UN);
            }, false);
            fclose($handle);
        }
    }

    private function recoverInterruptedPublication($runtime) {
        $statePath = $this->statePath($runtime);
        if (!file_exists($statePath) && !is_link($statePath)) {
            return true;
        }
        if (is_link($statePath) || is_dir($statePath)) {
            return false;
        }

        $state = $this->readJson($statePath);
        $transactionId = is_array($state) ? (string)($state['transaction'] ?? '') : '';
        if ((int)($state['format'] ?? 0) !== self::MANIFEST_FORMAT
            || !preg_match('/^[a-f0-9]{32}$/', $transactionId)
        ) {
            return false;
        }

        if ((string)($state['phase'] ?? '') === 'committed'
            && $this->committedStateMatchesRuntime($runtime, $state)
        ) {
            if (!$this->writeManagedRuntimeMarkers($runtime)) {
                return false;
            }
            if (!$this->removeFile($statePath)) {
                return false;
            }
            $this->removeTree($this->transactionsPath($runtime) . '/' . $transactionId);
            return true;
        }

        $transaction = $this->transactionsPath($runtime) . '/' . $transactionId;
        if (!$this->restoreTransactionBackup($runtime, $transaction)) {
            return false;
        }
        if (!$this->removeFile($statePath)) {
            return false;
        }
        $this->removeTree($transaction);
        return true;
    }

    private function committedStateMatchesRuntime($runtime, array $state) {
        $manifest = $this->readJson($this->publishedManifestPath($runtime));
        return $this->validManifest($manifest)
            && hash_equals((string)($state['expected_version'] ?? ''), (string)($manifest['version'] ?? ''))
            && hash_equals((string)($state['source_digest'] ?? ''), (string)($manifest['digest'] ?? ''))
            && $this->validManagedRuntimeMarker($runtime)
            && $this->treeMatchesManifest($runtime, $manifest, true);
    }

    private function restoreTransactionBackup($runtime, $transaction) {
        if ($transaction === '' || !is_dir($transaction) || is_link($transaction)) {
            return false;
        }
        if (!$this->removeFile($transaction . '/growth-reserve.bin')
            || !$this->removeFile($transaction . '/rollback-reserve.bin')
        ) {
            return false;
        }
        $backup = $transaction . '/backup';
        $manifest = $this->readJson($transaction . '/backup-manifest.json');
        if (!$this->validManifest($manifest)
            || !$this->treeMatchesManifest($backup, $manifest, false)
            || !$this->applyManifest($backup, $runtime, $manifest)
            || !$this->removeFilesMissingFromManifest($runtime, $manifest)
            || !$this->treeMatchesManifest($runtime, $manifest, true)
        ) {
            return false;
        }

        if (!$this->restorePublicationMetadata($runtime, $transaction)) {
            return false;
        }
        return $this->clearRuntimeClassmapCaches($runtime);
    }

    private function cleanupOrphanTransactions($runtime) {
        $state = $this->statePath($runtime);
        if (file_exists($state) || is_link($state)) {
            return false;
        }
        $root = $this->transactionsPath($runtime);
        $items = $this->scanDirectory($root);
        if ($items === false) {
            return false;
        }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $path = $root . '/' . $item;
            if (!preg_match('/^[a-f0-9]{32}$/', $item)
                || !$this->removeTree($path)
            ) {
                return false;
            }
        }
        return true;
    }

    private function createTransaction($runtime) {
        try {
            $id = bin2hex(random_bytes(16));
        } catch (\Throwable $e) {
            return '';
        }
        $path = $this->transactionsPath($runtime) . '/' . $id;
        return $this->createDirectory($path, 0700)
            && $this->hardenDirectory($path, 0700, 0077)
            ? $path
            : '';
    }

    private function snapshotPublicationMetadata($runtime, $transaction) {
        $directory = rtrim($transaction, '/\\') . '/publication-metadata';
        if (!$this->createDirectory($directory, 0700)
            || !$this->hardenDirectory($directory, 0700, 0077)
        ) {
            return false;
        }

        $files = array();
        $paths = array(
            'easychat-code-manifest.json' => $this->publishedManifestPath($runtime),
            'easychat-publish-managed.json' => $this->managedMarkerPath($runtime),
            'easychat-runtime-publisher.json' => $this->durableManagedMarkerPath($runtime),
        );
        foreach ($paths as $name => $source) {
            if (!file_exists($source) && !is_link($source)) {
                continue;
            }
            if (!is_file($source) || is_link($source)) {
                return false;
            }
            $hash = $this->hashFile($source);
            $size = $this->safeCall(function () use ($source) {
                return filesize($source);
            }, false);
            $permissions = $this->safeCall(function () use ($source) {
                return fileperms($source);
            }, false);
            if ($hash === false || $size === false || $permissions === false) {
                return false;
            }
            $entry = array(
                'sha256' => $hash,
                'size' => (int)$size,
                'mode' => (int)($permissions & 0777),
            );
            if (!$this->validManifestEntry($entry)
                || !$this->copyFileAtomic($source, $directory . '/' . $name, $entry, false)
            ) {
                return false;
            }
            $files[$name] = $entry;
        }
        ksort($files, SORT_STRING);
        $encoded = json_encode($files, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return false;
        }
        $manifest = array(
            'digest' => hash('sha256', $encoded),
            'files' => $files,
        );
        return $this->writeJsonAtomic($transaction . '/publication-metadata-manifest.json', $manifest, 0600)
            ? $manifest
            : false;
    }

    private function restorePublicationMetadata($runtime, $transaction) {
        $manifest = $this->readJson($transaction . '/publication-metadata-manifest.json');
        if (!$this->validManifest($manifest)) {
            return false;
        }
        $directory = rtrim($transaction, '/\\') . '/publication-metadata';
        $paths = array(
            'easychat-code-manifest.json' => $this->publishedManifestPath($runtime),
            'easychat-publish-managed.json' => $this->managedMarkerPath($runtime),
            'easychat-runtime-publisher.json' => $this->durableManagedMarkerPath($runtime),
        );
        foreach ($manifest['files'] as $name => $entry) {
            if (!isset($paths[$name])
                || !$this->fileMatchesEntry($directory . '/' . $name, $entry, true)
            ) {
                return false;
            }
        }
        foreach ($paths as $name => $destination) {
            if (!isset($manifest['files'][$name])) {
                if (!$this->removeFile($destination)) {
                    return false;
                }
                continue;
            }
            if (!$this->copyFileAtomic(
                $directory . '/' . $name,
                $destination,
                $manifest['files'][$name],
                false
            )) {
                return false;
            }
        }
        return true;
    }

    private function manifestBytes(array $manifest) {
        $total = 0;
        foreach ($manifest['files'] as $entry) {
            $total += max(0, (int)$entry['size']);
        }
        return $total;
    }

    private function largestManifestFile(array $manifest) {
        $largest = 0;
        foreach ($manifest['files'] as $entry) {
            $largest = max($largest, max(0, (int)$entry['size']));
        }
        return $largest;
    }

    private function createAllocatedReserve($path, $bytes) {
        $bytes = max(0, (int)$bytes);
        if (is_link($path) || is_dir($path)) {
            return false;
        }
        $handle = $this->safeCall(function () use ($path) {
            return fopen($path, 'xb');
        }, false);
        if (!is_resource($handle)) {
            return false;
        }

        $remaining = $bytes;
        $ok = true;
        try {
            while ($remaining > 0) {
                $length = min(65536, $remaining);
                $chunk = random_bytes($length);
                $written = $this->safeCall(function () use ($handle, $chunk) {
                    return fwrite($handle, $chunk);
                }, false);
                if ($written !== $length) {
                    $ok = false;
                    break;
                }
                $remaining -= $written;
            }
            $ok = $ok && $this->safeCall(function () use ($handle) {
                return fflush($handle);
            }, false);
            if ($ok && function_exists('fsync')) {
                $ok = $this->safeCall(function () use ($handle) {
                    return fsync($handle);
                }, false) === true;
            }
        } catch (\Throwable $e) {
            $ok = false;
        } finally {
            fclose($handle);
        }

        clearstatcache(true, $path);
        $size = $this->safeCall(function () use ($path) {
            return filesize($path);
        }, false);
        $ok = $ok
            && $size !== false
            && (int)$size === $bytes
            && $this->hardenFile($path, 0600, 0077);
        if (!$ok) {
            $this->removeFile($path);
        }
        return $ok;
    }

    private function buildTreeManifest($root, $protectRuntimePaths, $skipBundleMetadata = false) {
        $root = rtrim((string)$root, '/\\');
        if (!is_dir($root) || is_link($root)) {
            return false;
        }
        $files = array();
        if (!$this->collectTreeFiles($root, $root, '', (bool)$protectRuntimePaths, (bool)$skipBundleMetadata, $files)) {
            return false;
        }
        ksort($files, SORT_STRING);
        $encoded = json_encode($files, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return false;
        }
        return array(
            'digest' => hash('sha256', $encoded),
            'files' => $files,
        );
    }

    private function collectTreeFiles($root, $directory, $relative, $protectRuntimePaths, $skipBundleMetadata, array &$files) {
        if (!is_dir($directory) || is_link($directory)) {
            return false;
        }
        $items = $this->scanDirectory($directory);
        if ($items === false) {
            return false;
        }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $childRelative = ltrim($relative . '/' . $item, '/');
            if ($protectRuntimePaths && $this->isRuntimePathProtected($childRelative)) {
                continue;
            }
            if ($skipBundleMetadata && $this->isBundleMetadataPath($childRelative)) {
                continue;
            }
            $path = $root . '/' . $childRelative;
            if (is_link($path)) {
                return false;
            }
            if (is_dir($path)) {
                if (!$this->collectTreeFiles($root, $path, $childRelative, $protectRuntimePaths, $skipBundleMetadata, $files)) {
                    return false;
                }
                continue;
            }
            if (!is_file($path)) {
                return false;
            }
            $hash = $this->hashFile($path);
            $size = $this->safeCall(function () use ($path) {
                return filesize($path);
            }, false);
            $permissions = $this->safeCall(function () use ($path) {
                return fileperms($path);
            }, false);
            if ($hash === false || $size === false || $permissions === false) {
                return false;
            }
            $files[$childRelative] = array(
                'sha256' => $hash,
                'size' => (int)$size,
                'mode' => (int)($permissions & 0777),
            );
        }
        return true;
    }

    private function materializeManifestBlobs($source, $blobRoot, array $manifest, $publicationSource = false) {
        foreach ($manifest['files'] as $relativePath => $entry) {
            $sourceFile = rtrim($source, '/\\') . '/' . $relativePath;
            if ($publicationSource && $relativePath === '.htaccess') {
                $sourceFile = rtrim($source, '/\\') . '/easychat-runtime-htaccess.txt';
            }
            $blob = rtrim($blobRoot, '/\\') . '/' . $entry['sha256'] . '.blob';
            if (is_file($blob)) {
                if (!$this->fileMatchesEntry($blob, $entry, false)) {
                    return false;
                }
                continue;
            }
            if (!$this->copyFileAtomic($sourceFile, $blob, $entry, false)) {
                return false;
            }
        }
        return true;
    }

    private function applyManifest($blobRoot, $runtime, array $manifest) {
        if (!$this->removeRuntimeShapeConflicts($runtime, $manifest)) {
            return false;
        }
        foreach ($this->orderedManifestPaths($manifest) as $relativePath) {
            $entry = $manifest['files'][$relativePath];
            $blob = rtrim($blobRoot, '/\\') . '/' . $entry['sha256'] . '.blob';
            $destination = rtrim($runtime, '/\\') . '/' . $relativePath;
            if (!$this->copyFileAtomic($blob, $destination, $entry, true)) {
                return false;
            }
            $this->checkpoint('after_apply_file:' . $relativePath);
        }
        return $this->hardenRuntimeDirectories($runtime, $manifest);
    }

    private function hardenRuntimeDirectories($runtime, array $manifest) {
        $directories = array('' => true);
        foreach (array_keys($manifest['files']) as $relativePath) {
            $directory = dirname($relativePath);
            while ($directory !== '.' && $directory !== '') {
                $directories[$directory] = true;
                $directory = dirname($directory);
            }
        }
        uksort($directories, function ($left, $right) {
            return substr_count($left, '/') <=> substr_count($right, '/');
        });
        foreach (array_keys($directories) as $relativePath) {
            $path = rtrim($runtime, '/\\') . ($relativePath === '' ? '' : '/' . $relativePath);
            if (!is_dir($path) || is_link($path)
                || !$this->safeCall(function () use ($path) {
                    return chmod($path, 0755);
                }, false)
            ) {
                return false;
            }
            clearstatcache(true, $path);
            $permissions = $this->safeCall(function () use ($path) {
                return fileperms($path);
            }, false);
            if ($permissions === false || ($permissions & 0777) !== 0755) {
                return false;
            }
        }
        return true;
    }

    private function orderedManifestPaths(array $manifest) {
        $paths = array_keys($manifest['files']);
        $readerBootstrap = 'core/runtime_reader_lock.php';
        $entrypoints = array(
            'version.php',
            'cron.php',
            'index.php',
            'cli/max_polling.php',
            'cli/max_qr_auth_worker.php',
        );
        usort($paths, function ($left, $right) use ($readerBootstrap, $entrypoints) {
            $leftPriority = $left === $readerBootstrap ? 0 : (in_array($left, $entrypoints, true) ? 2 : 1);
            $rightPriority = $right === $readerBootstrap ? 0 : (in_array($right, $entrypoints, true) ? 2 : 1);
            if ($leftPriority !== $rightPriority) {
                return $leftPriority <=> $rightPriority;
            }
            return strcmp($left, $right);
        });
        return $paths;
    }

    private function removeRuntimeShapeConflicts($runtime, array $manifest) {
        $directories = array();
        foreach (array_keys($manifest['files']) as $relativePath) {
            $directory = dirname($relativePath);
            while ($directory !== '.' && $directory !== '') {
                $directories[$directory] = true;
                $directory = dirname($directory);
            }
        }
        uksort($directories, function ($left, $right) {
            return substr_count($left, '/') <=> substr_count($right, '/');
        });
        foreach (array_keys($directories) as $relativePath) {
            $path = rtrim($runtime, '/\\') . '/' . $relativePath;
            if (is_link($path)) {
                return false;
            }
            if (file_exists($path) && !is_dir($path) && !$this->removeRuntimeFile($path)) {
                return false;
            }
            if (!is_dir($path) && !$this->createDirectory($path, 0755)) {
                return false;
            }
        }
        foreach (array_keys($manifest['files']) as $relativePath) {
            $path = rtrim($runtime, '/\\') . '/' . $relativePath;
            if (is_link($path)) {
                return false;
            }
            if (is_dir($path) && !$this->removeTree($path)) {
                return false;
            }
        }
        return true;
    }

    private function removeFilesMissingFromManifest($runtime, array $manifest) {
        $current = $this->buildTreeManifest($runtime, true);
        if ($current === false) {
            return false;
        }
        foreach (array_keys($current['files']) as $relativePath) {
            if (isset($manifest['files'][$relativePath])) {
                continue;
            }
            if (!$this->removeRuntimeFile(rtrim($runtime, '/\\') . '/' . $relativePath)) {
                return false;
            }
        }
        return $this->pruneEmptyRuntimeDirectories($runtime, $runtime, '');
    }

    private function pruneEmptyRuntimeDirectories($root, $directory, $relative) {
        $items = $this->scanDirectory($directory);
        if ($items === false) {
            return false;
        }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $childRelative = ltrim($relative . '/' . $item, '/');
            if ($this->isRuntimePathProtected($childRelative)) {
                continue;
            }
            $path = $root . '/' . $childRelative;
            if (is_link($path)) {
                return false;
            }
            if (is_dir($path) && !$this->pruneEmptyRuntimeDirectories($root, $path, $childRelative)) {
                return false;
            }
        }
        if ($relative === '') {
            return true;
        }
        $remaining = $this->scanDirectory($directory);
        if ($remaining === false) {
            return false;
        }
        if (count($remaining) === 2) {
            return (bool)$this->safeCall(function () use ($directory) {
                return rmdir($directory);
            }, false);
        }
        return true;
    }

    private function treeMatchesManifest($root, array $manifest, $protectRuntimePaths) {
        if (!$this->validManifest($manifest)) {
            return false;
        }
        if (!$protectRuntimePaths) {
            foreach ($manifest['files'] as $entry) {
                $blob = rtrim($root, '/\\') . '/' . $entry['sha256'] . '.blob';
                if (!$this->fileMatchesEntry($blob, $entry, false)) {
                    return false;
                }
            }
            return true;
        }
        $actual = $this->buildTreeManifest($root, true);
        return $actual !== false
            && hash_equals((string)$manifest['digest'], (string)$actual['digest'])
            && $manifest['files'] === $actual['files']
            && $this->runtimeDirectoriesMatchPolicy($root, $manifest);
    }

    private function runtimeDirectoriesMatchPolicy($runtime, array $manifest) {
        $directories = array('' => true);
        foreach (array_keys($manifest['files']) as $relativePath) {
            $directory = dirname($relativePath);
            while ($directory !== '.' && $directory !== '') {
                $directories[$directory] = true;
                $directory = dirname($directory);
            }
        }
        foreach (array_keys($directories) as $relativePath) {
            $path = rtrim($runtime, '/\\') . ($relativePath === '' ? '' : '/' . $relativePath);
            $permissions = $this->safeCall(function () use ($path) {
                return fileperms($path);
            }, false);
            if (!is_dir($path) || is_link($path)
                || $permissions === false
                || ($permissions & 0777) !== 0755
            ) {
                return false;
            }
        }
        return true;
    }

    private function validManifest($manifest) {
        if (!is_array($manifest)
            || !isset($manifest['digest'], $manifest['files'])
            || !is_string($manifest['digest'])
            || !preg_match('/^[a-f0-9]{64}$/', $manifest['digest'])
            || !is_array($manifest['files'])
        ) {
            return false;
        }
        $normalized = array();
        foreach ($manifest['files'] as $relativePath => $entry) {
            $path = $this->normalizeRelativePath($relativePath);
            if ($path === '' || $path !== $relativePath || !$this->validManifestEntry($entry)) {
                return false;
            }
            $normalized[$path] = $entry;
        }
        ksort($normalized, SORT_STRING);
        $encoded = json_encode($normalized, JSON_UNESCAPED_SLASHES);
        return is_string($encoded)
            && hash_equals($manifest['digest'], hash('sha256', $encoded));
    }

    private function validManifestEntry($entry) {
        return is_array($entry)
            && isset($entry['sha256'], $entry['size'], $entry['mode'])
            && is_string($entry['sha256'])
            && preg_match('/^[a-f0-9]{64}$/', $entry['sha256'])
            && is_int($entry['size'])
            && $entry['size'] >= 0
            && is_int($entry['mode'])
            && $entry['mode'] >= 0
            && $entry['mode'] <= 0777;
    }

    private function fileMatchesEntry($path, array $entry, $checkMode = true) {
        if (!is_file($path) || is_link($path)) {
            return false;
        }
        $size = $this->safeCall(function () use ($path) {
            return filesize($path);
        }, false);
        $permissions = $this->safeCall(function () use ($path) {
            return fileperms($path);
        }, false);
        return $size !== false
            && (int)$size === (int)$entry['size']
            && (!$checkMode || ($permissions !== false && ($permissions & 0777) === (int)$entry['mode']))
            && ($hash = $this->hashFile($path)) !== false
            && hash_equals($entry['sha256'], $hash);
    }

    private function copyFileAtomic($source, $destination, array $entry, $invalidatePhp) {
        if (!$this->fileMatchesEntry($source, $entry, false)
            || is_link($destination)
            || is_dir($destination)
        ) {
            return false;
        }
        $directory = dirname($destination);
        if (!$this->ensureDirectoryChain($directory)) {
            return false;
        }
        $temporary = $this->safeCall(function () use ($directory) {
            return tempnam($directory, '.easychat-tx-');
        }, false);
        if ($temporary === false) {
            return false;
        }
        $ok = $this->safeCall(function () use ($source, $temporary) {
            return copy($source, $temporary);
        }, false)
            && $this->fileMatchesEntry($temporary, $entry, false)
            && $this->safeCall(function () use ($temporary, $entry) {
                return chmod($temporary, (int)$entry['mode']);
            }, false)
            && $this->safeCall(function () use ($temporary, $destination) {
                return rename($temporary, $destination);
            }, false);
        if (!$ok) {
            $this->removeFile($temporary);
            return false;
        }
        return !$invalidatePhp || $this->invalidatePhp($destination);
    }

    private function removeRuntimeFile($path) {
        if (!file_exists($path) && !is_link($path)) {
            return true;
        }
        if (is_link($path) || is_dir($path)) {
            return false;
        }
        return $this->invalidatePhp($path) && $this->removeFile($path);
    }

    private function clearRuntimeClassmapCaches($runtime) {
        foreach (array(
            rtrim($runtime, '/\\') . '/core/autoload_classmap.json',
            rtrim($runtime, '/\\') . '/log/autoload_classmap.json',
        ) as $path) {
            if ((file_exists($path) || is_link($path)) && !$this->removeFile($path)) {
                return false;
            }
        }
        return true;
    }

    private function invalidatePhp($path) {
        if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'php') {
            return true;
        }
        $canonical = $this->safeCall(function () use ($path) {
            return realpath($path);
        }, false);
        $opcachePath = is_string($canonical) && $canonical !== '' ? $canonical : $path;
        if (!function_exists('opcache_invalidate')) {
            return !$this->opcacheRequiresExplicitInvalidation();
        }
        if (function_exists('opcache_is_script_cached')) {
            $cached = $this->safeCall(function () use ($opcachePath) {
                return opcache_is_script_cached($opcachePath);
            }, null);
            if ($cached === false) {
                return true;
            }
        }
        return $this->safeCall(function () use ($opcachePath) {
            return opcache_invalidate($opcachePath, true);
        }, false) === true;
    }

    private function opcacheRequiresExplicitInvalidation() {
        $enabled = $this->iniFlag('opcache.enable', false);
        $validatesTimestamps = $this->iniFlag('opcache.validate_timestamps', true);
        return $enabled && !$validatesTimestamps;
    }

    private function iniFlag($name, $default) {
        $value = $this->safeCall(function () use ($name) {
            return ini_get($name);
        }, false);
        if ($value === false || $value === '') {
            return (bool)$default;
        }
        return !in_array(strtolower(trim((string)$value)), array('0', 'off', 'false', 'no'), true);
    }

    private function verifiedBundledSourceManifest($source, $expectedVersion) {
        $definition = $this->readVerifiedBundleDefinition($source, $expectedVersion);
        if ($definition === false) {
            return false;
        }
        $expectation = $definition['expectation'];
        $manifest = $definition['manifest'];

        $actual = $this->buildTreeManifest($source, true, true);
        $actualContent = $actual === false ? false : $this->contentOnlyManifest($actual);
        if ($actualContent === false || !$this->sourceVersionMatches($source, $expectation['version'])) {
            return false;
        }
        $filteredFiles = array();
        foreach ($manifest['files'] as $relativePath => $entry) {
            if (!isset($actualContent['files'][$relativePath])
                || $actualContent['files'][$relativePath] !== $entry
                || !isset($actual['files'][$relativePath])
            ) {
                return false;
            }
            $filteredFiles[$relativePath] = $actual['files'][$relativePath];
        }

        ksort($filteredFiles, SORT_STRING);
        $encoded = json_encode($filteredFiles, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return false;
        }
        return $this->publicationManifestFromBundle(array(
            'digest' => hash('sha256', $encoded),
            'files' => $filteredFiles,
        ));
    }

    private function readVerifiedBundleDefinition($source, $expectedVersion) {
        $expectation = $this->readBundleExpectation($source);
        if ($expectation === false
            || !hash_equals((string)$expectedVersion, $expectation['version'])
            || !hash_equals(self::PUBLISH_REVISION, $expectation['publish_revision'])
        ) {
            return false;
        }

        $manifestPath = $this->bundleManifestPath($source);
        $manifestHash = $this->hashFile($manifestPath);
        $manifest = $this->readJson($manifestPath);
        if ($manifestHash === false
            || !hash_equals($expectation['manifest_sha256'], $manifestHash)
            || !$this->validBundleManifest($manifest)
            || !hash_equals($expectation['version'], (string)$manifest['version'])
            || !hash_equals($expectation['publish_revision'], (string)$manifest['publish_revision'])
            || !hash_equals($expectation['digest'], (string)$manifest['digest'])
        ) {
            return false;
        }

        return array(
            'expectation' => $expectation,
            'manifest' => $manifest,
        );
    }

    private function readBundleExpectation($source) {
        $expectation = $this->readJson($this->bundleExpectationPath($source));
        if (!is_array($expectation)
            || (int)($expectation['format'] ?? 0) !== self::MANIFEST_FORMAT
            || trim((string)($expectation['version'] ?? '')) === ''
            || !hash_equals(self::PUBLISH_REVISION, (string)($expectation['publish_revision'] ?? ''))
            || !preg_match('/^[a-f0-9]{64}$/', (string)($expectation['manifest_sha256'] ?? ''))
            || !preg_match('/^[a-f0-9]{64}$/', (string)($expectation['digest'] ?? ''))
        ) {
            return false;
        }
        return array(
            'format' => self::MANIFEST_FORMAT,
            'version' => trim((string)$expectation['version']),
            'publish_revision' => (string)$expectation['publish_revision'],
            'manifest_sha256' => (string)$expectation['manifest_sha256'],
            'digest' => (string)$expectation['digest'],
        );
    }

    private function validBundleManifest($manifest) {
        if (!is_array($manifest)
            || (int)($manifest['format'] ?? 0) !== self::MANIFEST_FORMAT
            || trim((string)($manifest['version'] ?? '')) === ''
            || !hash_equals(self::PUBLISH_REVISION, (string)($manifest['publish_revision'] ?? ''))
            || !isset($manifest['digest'], $manifest['files'])
            || !preg_match('/^[a-f0-9]{64}$/', (string)$manifest['digest'])
            || !is_array($manifest['files'])
        ) {
            return false;
        }
        $files = array();
        foreach ($manifest['files'] as $relativePath => $entry) {
            $path = $this->normalizeRelativePath($relativePath);
            if ($path === ''
                || $path !== $relativePath
                || $this->isBundleMetadataPath($path)
                || !is_array($entry)
                || !isset($entry['sha256'], $entry['size'])
                || !preg_match('/^[a-f0-9]{64}$/', (string)$entry['sha256'])
                || !is_int($entry['size'])
                || $entry['size'] < 0
            ) {
                return false;
            }
            $files[$path] = array(
                'sha256' => (string)$entry['sha256'],
                'size' => (int)$entry['size'],
            );
        }
        ksort($files, SORT_STRING);
        $encoded = json_encode($files, JSON_UNESCAPED_SLASHES);
        return is_string($encoded)
            && hash_equals((string)$manifest['digest'], hash('sha256', $encoded));
    }

    private function contentOnlyManifest(array $manifest) {
        if (!$this->validManifest($manifest)) {
            return false;
        }
        $files = array();
        foreach ($manifest['files'] as $relativePath => $entry) {
            $files[$relativePath] = array(
                'sha256' => (string)$entry['sha256'],
                'size' => (int)$entry['size'],
            );
        }
        ksort($files, SORT_STRING);
        $encoded = json_encode($files, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return false;
        }
        return array(
            'digest' => hash('sha256', $encoded),
            'files' => $files,
        );
    }

    private function publicationManifestFromBundle(array $manifest) {
        if (!$this->validManifest($manifest)
            || !isset($manifest['files']['easychat-runtime-htaccess.txt'])
        ) {
            return false;
        }
        $files = $manifest['files'];
        $htaccess = $files['easychat-runtime-htaccess.txt'];
        unset($files['easychat-runtime-htaccess.txt']);
        $files['.htaccess'] = $htaccess;
         
         
         
        foreach ($files as &$entry) {
            $entry['mode'] = 0644;
        }
        unset($entry);
        ksort($files, SORT_STRING);
        $encoded = json_encode($files, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return false;
        }
        return array(
            'digest' => hash('sha256', $encoded),
            'files' => $files,
        );
    }

    private function sourceVersionMatches($source, $expectedVersion) {
        if ($expectedVersion === '') {
            return false;
        }
        $versionFile = rtrim($source, '/\\') . '/version.php';
        $contents = $this->safeCall(function () use ($versionFile) {
            return file_get_contents($versionFile);
        }, false);
        return is_string($contents)
            && preg_match("/define\\('EASYCHAT_VERSION',\\s*'([^']+)'\\);/", $contents, $matches)
            && hash_equals($expectedVersion, (string)$matches[1]);
    }

    private function normalizeRelativePath($path) {
        $path = str_replace('\\', '/', trim((string)$path));
        if ($path === '' || $path[0] === '/' || strpos($path, "\0") !== false) {
            return '';
        }
        $parts = explode('/', $path);
        foreach ($parts as $part) {
            if ($part === '' || $part === '.' || $part === '..') {
                return '';
            }
        }
        return implode('/', $parts);
    }

    






    private function normalizeMigrationExpectation(array $expectation, array $permanentFiles) {
        $migrationFiles = $expectation['migration_files'] ?? array();
        $migrationDigest = $expectation['migration_digest'] ?? hash('sha256', '[]');
        if (!is_array($migrationFiles)
            || !is_string($migrationDigest)
            || !preg_match('/^[a-f0-9]{64}$/', $migrationDigest)
        ) {
            $this->fail('adapter_migration_expectation_invalid');
            return false;
        }

        $prefixes = array(
            'admin/controller/extension/module/easychat/00_impl_r' . self::PUBLISH_REVISION . '_',
            'admin/controller/extension/module/easychat/recovery_v3/00_impl_r' . self::PUBLISH_REVISION . '_',
            'catalog/controller/extension/module/easychat/00_impl_r' . self::PUBLISH_REVISION . '_',
        );
        $pointers = array(
            'admin/controller/extension/module/easychat/zz_current.json',
            'admin/controller/extension/module/easychat/recovery_v3/zz_current.json',
            'catalog/controller/extension/module/easychat/zz_current.json',
        );
        $normalized = array();
        $implementationCounts = array_fill(0, count($prefixes), 0);
        foreach ($migrationFiles as $relativePath => $entry) {
            $path = $this->normalizeRelativePath($relativePath);
            if ($path === '' || $path !== $relativePath || isset($permanentFiles[$path])
                || !is_array($entry) || count($entry) !== 2
                || !isset($entry['sha256'], $entry['size'])
                || !is_string($entry['sha256'])
                || !preg_match('/^[a-f0-9]{64}$/', $entry['sha256'])
                || !is_int($entry['size']) || $entry['size'] < 1
            ) {
                $this->fail('adapter_migration_expectation_invalid');
                return false;
            }
            $allowed = in_array($path, $pointers, true);
            foreach ($prefixes as $index => $prefix) {
                if (strpos($path, $prefix) === 0
                    && preg_match('#^' . preg_quote($prefix, '#') . '([a-f0-9]{64})\\.php$#', $path, $matches)
                    && hash_equals($matches[1], $entry['sha256'])
                ) {
                    $implementationCounts[$index]++;
                    $allowed = true;
                }
            }
            if (!$allowed) {
                $this->fail('adapter_migration_expectation_invalid');
                return false;
            }
            $normalized[$path] = $entry;
        }
        if ($migrationFiles && (count($migrationFiles) !== 6 || $implementationCounts !== array(1, 1, 1))) {
            $this->fail('adapter_migration_expectation_invalid');
            return false;
        }

        ksort($normalized, SORT_STRING);
        $encoded = json_encode($normalized, JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded) || !hash_equals($migrationDigest, hash('sha256', $encoded))) {
            $this->fail('adapter_migration_digest_invalid');
            return false;
        }
        return $normalized;
    }

    private function normalizeInstallHistoryPath($path, array $aliases) {
        $path = str_replace('\\', '/', trim((string)$path));
        $path = rtrim(ltrim($path, '/'), '/');
        if (strpos($path, 'upload/') === 0) {
            $path = substr($path, 7);
        }
        $parts = explode('/', $path);
        if (!$parts || !isset($aliases[$parts[0]])) {
            return $this->normalizeRelativePath($path);
        }
        $parts[0] = $aliases[$parts[0]];
        return $this->normalizeRelativePath(implode('/', $parts));
    }

    private function isEasyChatHistorySentinel($path, array $expectedPaths) {
        return isset($expectedPaths[$path]) || in_array($path, array(
            'admin/controller/module/easychat.php',
            'admin/controller/extension/module/easychat.php',
            'catalog/controller/module/easychat.php',
            'catalog/controller/extension/module/easychat.php',
            'system/library/easychat/version.php',
            'system/library/easychat_bundle_r3/version.php',
            'easychat/index.php',
        ), true);
    }

    private function isAllowlistedEasyChatHistoryPath($path, array $expectedPaths, array $expectedAncestors) {
        if (isset($expectedPaths[$path]) || isset($expectedAncestors[$path])) {
            return true;
        }
        return preg_match('#^(?:'
            . 'admin(?:/controller(?:/(?:extension/)?module(?:/easychat(?:\\.php|/.*)?)?)?)?'
            . '|catalog(?:/controller(?:/(?:extension/)?module(?:/easychat(?:\\.php|/.*)?)?)?)?'
            . '|admin/language/[a-z0-9_-]+(?:/(?:extension/)?module(?:/easychat\\.php)?)?'
            . '|admin/view/template(?:/(?:extension/)?module(?:/easychat\\.(?:tpl|twig))?)?'
            . '|catalog/view/theme/[a-zA-Z0-9_-]+/template(?:/(?:extension/)?module(?:/easychat\\.(?:tpl|twig))?)?'
            . '|system(?:/library(?:/easychat(?:/.*|_[a-zA-Z0-9_./-]+|\\.php)?)?)?'
            . '|easychat(?:/.*)?'
            . ')$#', $path) === 1;
    }

    private function queryRows($db, $sql) {
        $result = $this->safeCall(function () use ($db, $sql) {
            return $db->query($sql);
        }, false);
        if ($result === false) {
            return false;
        }
        if (is_object($result) && isset($result->rows) && is_array($result->rows)) {
            return $result->rows;
        }
        if (is_array($result)) {
            return $result;
        }
         
        return array();
    }

    private function pathsOverlap($source, $runtime) {
        $sourcePath = $this->canonicalProspectivePath($source);
        $runtimePath = $this->canonicalProspectivePath($runtime);
        if ($sourcePath === '' || $runtimePath === '') {
            return true;
        }
        if (DIRECTORY_SEPARATOR === '\\') {
            $sourcePath = strtolower($sourcePath);
            $runtimePath = strtolower($runtimePath);
        }
        return $sourcePath === $runtimePath
            || strpos($sourcePath . '/', $runtimePath . '/') === 0
            || strpos($runtimePath . '/', $sourcePath . '/') === 0;
    }

    private function canonicalProspectivePath($path) {
        $path = rtrim((string)$path, '/\\');
        if ($path === '') {
            return '';
        }
        $tail = array();
        $cursor = $path;
        while (!file_exists($cursor) && !is_link($cursor)) {
            $parent = dirname($cursor);
            if ($parent === $cursor) {
                return '';
            }
            array_unshift($tail, basename($cursor));
            $cursor = $parent;
        }
        $base = realpath($cursor);
        if ($base === false) {
            return '';
        }
        $canonical = rtrim(str_replace('\\', '/', $base), '/');
        foreach ($tail as $part) {
            if ($part === '' || $part === '.' || $part === '..') {
                return '';
            }
            $canonical .= '/' . $part;
        }
        return $canonical;
    }

    private function isRuntimePathProtected($relativePath) {
        $relativePath = $this->normalizeRelativePath($relativePath);
        if ($relativePath === '') {
            return false;
        }
        if ($relativePath === 'config.php'
            || $relativePath === '.env'
            || $relativePath === '.easychat-runtime-publisher.json'
            || $relativePath === 'core/autoload_classmap.json'
            || $relativePath === 'images/avatars'
            || strpos($relativePath, 'images/avatars/') === 0
            || $relativePath === 'storage/phpmax'
            || strpos($relativePath, 'storage/phpmax/') === 0
        ) {
            return true;
        }
        $topLevel = explode('/', $relativePath, 2)[0];
        return in_array($topLevel, array('uploads', 'log', 'logs', 'scratch'), true);
    }

    private function isBundleMetadataPath($relativePath) {
        $relativePath = $this->normalizeRelativePath($relativePath);
        if (in_array($relativePath, array(
            'easychat-bundle-manifest.json',
            '.easychat-bundle-complete.json',
        ), true)) {
            return true;
        }
        foreach (explode('/', $relativePath) as $part) {
            if ($part !== '' && $part[0] === '.') {
                return true;
            }
        }
        return false;
    }

    private function ensureDirectoryChain($directory) {
        if (is_link($directory)) {
            return false;
        }
        if (is_dir($directory)) {
            return realpath($directory) !== false;
        }
        $parent = dirname($directory);
        return $parent !== $directory
            && $this->ensureDirectoryChain($parent)
            && $this->createDirectory($directory, 0755);
    }

    private function createDirectory($directory, $mode) {
        if (is_link($directory)) {
            return false;
        }
        if (is_dir($directory)) {
            return true;
        }
        return (bool)$this->safeCall(function () use ($directory, $mode) {
            return mkdir($directory, $mode, true);
        }, false) || is_dir($directory);
    }

    private function hardenDirectory($directory, $mode, $forbiddenBits) {
        if (!is_dir($directory) || is_link($directory)) {
            return false;
        }
        $this->safeCall(function () use ($directory, $mode) {
            return chmod($directory, $mode);
        }, false);
        clearstatcache(true, $directory);
        if (DIRECTORY_SEPARATOR === '\\') {
            return true;
        }
        $permissions = $this->safeCall(function () use ($directory) {
            return fileperms($directory);
        }, false);
        return $permissions !== false
            && (($permissions & (int)$forbiddenBits) === 0);
    }

    private function hardenFile($path, $mode, $forbiddenBits) {
        if (!is_file($path) || is_link($path)) {
            return false;
        }
        $this->safeCall(function () use ($path, $mode) {
            return chmod($path, $mode);
        }, false);
        clearstatcache(true, $path);
        if (DIRECTORY_SEPARATOR === '\\') {
            return true;
        }
        $permissions = $this->safeCall(function () use ($path) {
            return fileperms($path);
        }, false);
        return $permissions !== false
            && (($permissions & (int)$forbiddenBits) === 0);
    }

    private function removeTree($path) {
        if (!file_exists($path) && !is_link($path)) {
            return true;
        }
        if (is_link($path) || !is_dir($path)) {
            return $this->removeFile($path);
        }
        $items = $this->scanDirectory($path);
        if ($items === false) {
            return false;
        }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            if (!$this->removeTree($path . '/' . $item)) {
                return false;
            }
        }
        return (bool)$this->safeCall(function () use ($path) {
            return rmdir($path);
        }, false);
    }

    private function removeFile($path) {
        if (!file_exists($path) && !is_link($path)) {
            return true;
        }
        if (is_dir($path) && !is_link($path)) {
            return false;
        }
        return (bool)$this->safeCall(function () use ($path) {
            return unlink($path);
        }, false);
    }

    private function writeJsonAtomic($path, array $value, $mode) {
        $json = json_encode($value, JSON_UNESCAPED_SLASHES);
        return is_string($json) && $this->writeFileAtomic($path, $json . "\n", $mode);
    }

    private function writeFileAtomic($path, $contents, $mode) {
        if (is_link($path) || is_dir($path) || !$this->ensureDirectoryChain(dirname($path))) {
            return false;
        }
        $temporary = $this->safeCall(function () use ($path) {
            return tempnam(dirname($path), '.easychat-meta-');
        }, false);
        if ($temporary === false) {
            return false;
        }
        $written = $this->safeCall(function () use ($temporary, $contents) {
            return file_put_contents($temporary, $contents, LOCK_EX);
        }, false);
        $ok = $written === strlen($contents)
            && hash_equals(hash('sha256', $contents), (string)$this->hashFile($temporary))
            && $this->safeCall(function () use ($temporary, $mode) {
                return chmod($temporary, $mode);
            }, false)
            && $this->safeCall(function () use ($temporary, $path) {
                return rename($temporary, $path);
            }, false);
        if (!$ok) {
            $this->removeFile($temporary);
        }
        return $ok;
    }

    private function readJson($path) {
        if (!is_file($path) || is_link($path)) {
            return false;
        }
        $contents = $this->safeCall(function () use ($path) {
            return file_get_contents($path);
        }, false);
        if (!is_string($contents) || strlen($contents) > 8 * 1024 * 1024) {
            return false;
        }
        $decoded = json_decode($contents, true);
        return is_array($decoded) ? $decoded : false;
    }

    private function scanDirectory($path) {
        return $this->safeCall(function () use ($path) {
            return scandir($path);
        }, false);
    }

    private function hashFile($path) {
        return $this->safeCall(function () use ($path) {
            return hash_file('sha256', $path);
        }, false);
    }

    private function lockPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-publish.lock';
    }

    private function statePath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-publish-state.json';
    }

    private function intentPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-publish.intent';
    }

    private function intentGuardPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-publish.intent.lock';
    }

    private function managedMarkerPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-publish-managed.json';
    }

    private function durableManagedMarkerPath($runtime) {
        return rtrim($runtime, '/\\') . '/.easychat-runtime-publisher.json';
    }

    private function publishedManifestPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-code-manifest.json';
    }

    private function transactionsPath($runtime) {
        return rtrim($runtime, '/\\') . '/log/easychat-publish-transactions';
    }

    private function bundleManifestPath($source) {
        return rtrim($source, '/\\') . '/easychat-bundle-manifest.json';
    }

    private function bundleExpectationPath($source) {
        return dirname(rtrim($source, '/\\')) . '/easychat_bundle_expectation.json';
    }

    private function fail($error) {
        $this->lastError = (string)$error;
        return false;
    }

    private function safeCall($callback, $fallback = false) {
        set_error_handler(function () {
            return true;
        });
        try {
            return call_user_func($callback);
        } catch (\Throwable $e) {
            return $fallback;
        } finally {
            restore_error_handler();
        }
    }
}
