<?php
namespace EasyChat\Library\Security;










final class LocalNativeFileSessionHandler extends \SessionHandler implements \SessionUpdateTimestampHandlerInterface {
    const GC_INTERVAL_SECONDS = 3600;
    const GC_SCAN_LIMIT = 2048;

    private $path;
    private $session_name;
    private $lifetime;
    private $directory_identity;
    private $revocation_path;
    private $revocation_identity;

    public function __construct($canonical_path, $session_name, $lifetime) {
        $canonical_path = (string)$canonical_path;
        $session_name = (string)$session_name;
        $lifetime = (int)$lifetime;
        $identity = $this->secureDirectoryIdentity($canonical_path);
        if ($canonical_path === '' || $session_name === '' || $lifetime < 1 || $identity === false) {
            throw new \RuntimeException('EasyChat session storage is not secure.');
        }

        $this->path = $canonical_path;
        $this->session_name = $session_name;
        $this->lifetime = $lifetime;
        $this->directory_identity = $identity;

        $revocation_path = dirname($canonical_path) . DIRECTORY_SEPARATOR . 'session-revocations';
        $revocation_identity = $this->prepareRevocationStorage($revocation_path, $identity['uid']);
        if ($revocation_identity === false) {
            throw new \RuntimeException('EasyChat session revocation storage is not secure.');
        }
        $this->revocation_path = $revocation_path;
        $this->revocation_identity = $revocation_identity;
    }

    public function isBoundTo($path, $session_name) {
        $real = realpath((string)$path);
        return $real !== false
            && hash_equals($this->path, $real)
            && hash_equals($this->session_name, (string)$session_name)
            && $this->isStorageHealthy();
    }

    public function isStorageHealthy() {
        $identity = $this->secureDirectoryIdentity($this->path);
        $revocation_identity = $this->secureDirectoryIdentity($this->revocation_path);
        return $identity !== false
            && $identity['dev'] === $this->directory_identity['dev']
            && $identity['ino'] === $this->directory_identity['ino']
            && $identity['uid'] === $this->directory_identity['uid']
            && $revocation_identity !== false
            && $revocation_identity['dev'] === $this->revocation_identity['dev']
            && $revocation_identity['ino'] === $this->revocation_identity['ino']
            && $revocation_identity['uid'] === $this->revocation_identity['uid'];
    }

    #[\ReturnTypeWillChange]
    public function open($ignored_global_path, $session_name) {
        if (!hash_equals($this->session_name, (string)$session_name) || !$this->isStorageHealthy()) {
            return false;
        }

        $opened = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($session_name) {
             
             
            return parent::open($this->path, (string)$session_name);
        }, false);
        if (!$opened) {
            return false;
        }

         
         
         
        $this->runThrottledGarbageCollection();
        return true;
    }

    #[\ReturnTypeWillChange]
    public function read($id) {
        $before = $this->sessionFileIdentity($id);
        if ($before === null) {
            return false;
        }

        $data = \EasyChat\Library\SafeRuntime::call(function () use ($id) {
            return parent::read((string)$id);
        }, false);
        if ($data === false) {
            return false;
        }

         
         
         
        $after = $this->sessionFileIdentity($id);
        if (!is_array($after) || (is_array($before) && !$this->sameInode($before, $after))) {
            return false;
        }
        return $data;
    }

    #[\ReturnTypeWillChange]
    public function write($id, $data) {
        if (!$this->validSessionId($id) || !$this->isStorageHealthy()) {
            return false;
        }
        $written = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($id, $data) {
            return parent::write((string)$id, (string)$data);
        }, false);
        return $written && is_array($this->sessionFileIdentity($id));
    }

    #[\ReturnTypeWillChange]
    public function destroy($id) {
        if (!$this->validSessionId($id)) {
            return false;
        }

        if (!$this->isSessionStorageHealthy()) {
             
             
            return $this->persistRevocation($id);
        }
        $identity = $this->sessionFileIdentity($id);
        if ($identity === null) {
             
             
             
             
             
            if ($this->quarantineUnsafeSessionPath($id)) {
                return true;
            }
            return $this->persistRevocation($id);
        }
        if ($identity === false) {
            return true;
        }

        $destroyed = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($id) {
            return parent::destroy((string)$id);
        }, false);
        clearstatcache(true, $this->sessionFilePath($id));
        if ($destroyed && lstat($this->sessionFilePath($id)) === false) {
            return true;
        }
        return $this->persistRevocation($id);
    }

    #[\ReturnTypeWillChange]
    public function gc($ignored_ini_lifetime) {
        return $this->collectGarbage();
    }

    #[\ReturnTypeWillChange]
    public function create_sid() {
        for ($attempt = 0; $attempt < 8; $attempt++) {
            try {
                $id = bin2hex(random_bytes(32));
            } catch (\Throwable $e) {
                throw new \RuntimeException('Unable to generate an EasyChat session id.', 0, $e);
            }
            clearstatcache(true, $this->sessionFilePath($id));
            if (lstat($this->sessionFilePath($id)) === false && !$this->isRevoked($id)) {
                return $id;
            }
        }
        throw new \RuntimeException('Unable to allocate a unique EasyChat session id.');
    }

    #[\ReturnTypeWillChange]
    public function validateId($id) {
        if ($this->isRevoked($id)) {
            return false;
        }
        $identity = $this->sessionFileIdentity($id);
        if (!is_array($identity)) {
            return false;
        }
        return $identity['mtime'] >= time() - $this->lifetime;
    }

    #[\ReturnTypeWillChange]
    public function updateTimestamp($id, $data) {
        if (!$this->validateId($id)) {
            return false;
        }
        $updated = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($id, $data) {
             
             
            return parent::write((string)$id, (string)$data);
        }, false);
        return $updated && is_array($this->sessionFileIdentity($id));
    }

    private function validSessionId($id) {
        return is_string($id) && preg_match('/\A[A-Za-z0-9,-]{22,256}\z/D', $id) === 1;
    }

    


    private function sessionFileIdentity($id) {
        if (!$this->validSessionId($id) || !$this->isStorageHealthy()) {
            return null;
        }
        $path = $this->sessionFilePath($id);
        clearstatcache(true, $path);
        $stat = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
            return lstat($path);
        }, false);
        if ($stat === false) {
            return false;
        }
        if (($stat['mode'] & 0170000) !== 0100000
            || (int)$stat['nlink'] !== 1
            || (int)$stat['uid'] !== $this->directory_identity['uid']
            || ($stat['mode'] & 0777) !== 0600
        ) {
            return null;
        }
        return array(
            'dev' => (int)$stat['dev'],
            'ino' => (int)$stat['ino'],
            'uid' => (int)$stat['uid'],
            'mode' => (int)$stat['mode'],
            'nlink' => (int)$stat['nlink'],
            'mtime' => (int)$stat['mtime'],
        );
    }

    private function sessionFilePath($id) {
        return $this->path . DIRECTORY_SEPARATOR . 'sess_' . (string)$id;
    }

    private function quarantineUnsafeSessionPath($id) {
        $path = $this->sessionFilePath($id);
        clearstatcache(true, $path);
        if (lstat($path) === false) {
            return true;
        }
        try {
            $quarantine = $this->path . DIRECTORY_SEPARATOR
                . '.easychat-invalid-session-' . bin2hex(random_bytes(16));
        } catch (\Throwable $e) {
            return false;
        }
        $renamed = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($path, $quarantine) {
            return rename($path, $quarantine);
        }, false);
        clearstatcache(true, $path);
        if (!$renamed || lstat($path) !== false) {
            return false;
        }

         
         
         
        if (!is_dir($quarantine) || is_link($quarantine)) {
            \EasyChat\Library\SafeRuntime::call(function () use ($quarantine) {
                return unlink($quarantine);
            }, false);
        }
        return true;
    }

    private function secureDirectoryIdentity($path) {
        $path = (string)$path;
        if ($path === '' || is_link($path) || realpath($path) !== $path || !is_dir($path) || !is_writable($path)) {
            return false;
        }
        clearstatcache(true, $path);
        $stat = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
            return lstat($path);
        }, false);
        if ($stat === false
            || ($stat['mode'] & 0170000) !== 0040000
            || ($stat['mode'] & 0777) !== 0700
        ) {
            return false;
        }
        return array(
            'dev' => (int)$stat['dev'],
            'ino' => (int)$stat['ino'],
            'uid' => (int)$stat['uid'],
        );
    }

    private function isSessionStorageHealthy() {
        $identity = $this->secureDirectoryIdentity($this->path);
        return $identity !== false
            && $identity['dev'] === $this->directory_identity['dev']
            && $identity['ino'] === $this->directory_identity['ino']
            && $identity['uid'] === $this->directory_identity['uid'];
    }

    private function isRevocationStorageHealthy() {
        $identity = $this->secureDirectoryIdentity($this->revocation_path);
        return $identity !== false
            && $identity['dev'] === $this->revocation_identity['dev']
            && $identity['ino'] === $this->revocation_identity['ino']
            && $identity['uid'] === $this->revocation_identity['uid'];
    }

    private function prepareRevocationStorage($path, $expected_uid) {
        if (is_link($path)) {
            return false;
        }
        if (!is_dir($path) && !\EasyChat\Library\SafeRuntime::mkdir($path, 0700, true)) {
            return false;
        }
        $real = realpath($path);
        if ($real === false || $real !== $path) {
            return false;
        }
        $mode = \EasyChat\Library\SafeRuntime::permissions($real);
        if ($mode !== 0700 && !\EasyChat\Library\SafeRuntime::chmod($real, 0700)) {
            return false;
        }
        $identity = $this->secureDirectoryIdentity($real);
        return $identity !== false && $identity['uid'] === (int)$expected_uid ? $identity : false;
    }

    private function revocationFilePath($id) {
        return $this->revocation_path . DIRECTORY_SEPARATOR
            . '.easychat-session-revoked-' . hash('sha256', (string)$id);
    }

    private function isRevoked($id) {
        if (!$this->validSessionId($id) || !$this->isRevocationStorageHealthy()) {
            return true;
        }
        $path = $this->revocationFilePath($id);
        clearstatcache(true, $path);
        $stat = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
            return lstat($path);
        }, false);
        if ($stat === false) {
            return false;
        }
         
         
        return true;
    }

    private function persistRevocation($id) {
        if (!$this->validSessionId($id) || !$this->isRevocationStorageHealthy()) {
            return false;
        }
        $path = $this->revocationFilePath($id);
        clearstatcache(true, $path);
        if (lstat($path) !== false) {
            return $this->privateRevocationFileIsSafe($path);
        }

        $previous_umask = umask(0077);
        try {
            $handle = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
                return fopen($path, 'x+b');
            }, false);
        } finally {
            umask($previous_umask);
        }
        if ($handle === false) {
            clearstatcache(true, $path);
            return lstat($path) !== false && $this->privateRevocationFileIsSafe($path);
        }
        $payload = (string)time();
        $written = fwrite($handle, $payload) === strlen($payload) && fflush($handle);
        $stat = fstat($handle);
        fclose($handle);
        clearstatcache(true, $path);
        return $written
            && is_array($stat)
            && ($stat['mode'] & 0170000) === 0100000
            && ($stat['mode'] & 0777) === 0600
            && (int)$stat['nlink'] === 1
            && (int)$stat['uid'] === $this->revocation_identity['uid']
            && $this->privateRevocationFileIsSafe($path);
    }

    private function privateRevocationFileIsSafe($path) {
        clearstatcache(true, $path);
        $stat = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
            return lstat($path);
        }, false);
        return is_array($stat)
            && ($stat['mode'] & 0170000) === 0100000
            && ($stat['mode'] & 0777) === 0600
            && (int)$stat['nlink'] === 1
            && (int)$stat['uid'] === $this->revocation_identity['uid'];
    }

    private function sameInode(array $left, array $right) {
        return $left['dev'] === $right['dev'] && $left['ino'] === $right['ino'];
    }

    private function runThrottledGarbageCollection() {
        $marker = $this->path . DIRECTORY_SEPARATOR . '.easychat-session-gc.lock';
        $handle = $this->openPrivateMarker($marker);
        if ($handle === false || !flock($handle, LOCK_EX | LOCK_NB)) {
            if (is_resource($handle)) {
                fclose($handle);
            }
            return;
        }

        $now = time();
        rewind($handle);
        $previous = trim((string)stream_get_contents($handle));
        if (ctype_digit($previous) && $now - (int)$previous < self::GC_INTERVAL_SECONDS) {
            flock($handle, LOCK_UN);
            fclose($handle);
            return;
        }

        rewind($handle);
        $marked = ftruncate($handle, 0)
            && fwrite($handle, (string)$now) === strlen((string)$now)
            && fflush($handle);
        if ($marked) {
            $this->collectGarbage();
        }
        flock($handle, LOCK_UN);
        fclose($handle);
    }

    private function openPrivateMarker($path) {
        clearstatcache(true, $path);
        $stat = lstat($path);
        if ($stat === false) {
            $previous_umask = umask(0077);
            try {
                $handle = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
                    return fopen($path, 'x+b');
                }, false);
            } finally {
                umask($previous_umask);
            }
        } else {
            $handle = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
                return fopen($path, 'r+b');
            }, false);
        }
        if ($handle === false) {
            return false;
        }

        $fstat = fstat($handle);
        clearstatcache(true, $path);
        $lstat = lstat($path);
        if ($fstat === false || $lstat === false
            || ($fstat['mode'] & 0170000) !== 0100000
            || (int)$fstat['nlink'] !== 1
            || (int)$fstat['uid'] !== $this->directory_identity['uid']
            || ($fstat['mode'] & 0777) !== 0600
            || (int)$fstat['dev'] !== (int)$lstat['dev']
            || (int)$fstat['ino'] !== (int)$lstat['ino']
        ) {
            fclose($handle);
            return false;
        }
        return $handle;
    }

    private function collectGarbage() {
        if (!$this->isStorageHealthy()) {
            return false;
        }
        $directory = \EasyChat\Library\SafeRuntime::call(function () {
            return opendir($this->path);
        }, false);
        if ($directory === false) {
            return false;
        }

        $deleted = 0;
        $scanned = 0;
        $cutoff = time() - $this->lifetime;
        while (($name = readdir($directory)) !== false && $scanned < self::GC_SCAN_LIMIT) {
            if (strpos($name, 'sess_') !== 0) {
                continue;
            }
            $id = substr($name, 5);
            if (!$this->validSessionId($id)) {
                continue;
            }
            $scanned++;
            if ($this->deleteExpiredUnlockedFile($id, $cutoff)) {
                $deleted++;
            }
        }
        closedir($directory);
        $this->collectExpiredRevocations($cutoff);
        return $deleted;
    }

    private function collectExpiredRevocations($cutoff) {
        if (!$this->isRevocationStorageHealthy()) {
            return;
        }
        $directory = \EasyChat\Library\SafeRuntime::call(function () {
            return opendir($this->revocation_path);
        }, false);
        if ($directory === false) {
            return;
        }
        $scanned = 0;
        while (($name = readdir($directory)) !== false && $scanned < self::GC_SCAN_LIMIT) {
            if (!preg_match('/\A\.easychat-session-revoked-[a-f0-9]{64}\z/D', $name)) {
                continue;
            }
            $scanned++;
            $path = $this->revocation_path . DIRECTORY_SEPARATOR . $name;
            clearstatcache(true, $path);
            $stat = lstat($path);
            if (!is_array($stat)
                || ($stat['mode'] & 0170000) !== 0100000
                || ($stat['mode'] & 0777) !== 0600
                || (int)$stat['nlink'] !== 1
                || (int)$stat['uid'] !== $this->revocation_identity['uid']
                || (int)$stat['mtime'] >= $cutoff
            ) {
                continue;
            }
            $handle = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
                return fopen($path, 'r+b');
            }, false);
            if ($handle === false || !flock($handle, LOCK_EX | LOCK_NB)) {
                if (is_resource($handle)) {
                    fclose($handle);
                }
                continue;
            }
            clearstatcache(true, $path);
            $after = lstat($path);
            $fstat = fstat($handle);
            if (is_array($after)
                && is_array($fstat)
                && (int)$after['dev'] === (int)$stat['dev']
                && (int)$after['ino'] === (int)$stat['ino']
                && (int)$fstat['dev'] === (int)$stat['dev']
                && (int)$fstat['ino'] === (int)$stat['ino']
                && (int)$after['mtime'] < $cutoff
            ) {
                \EasyChat\Library\SafeRuntime::call(function () use ($path) {
                    return unlink($path);
                }, false);
            }
            flock($handle, LOCK_UN);
            fclose($handle);
        }
        closedir($directory);
    }

    private function deleteExpiredUnlockedFile($id, $cutoff) {
        $identity = $this->sessionFileIdentity($id);
        if ($identity === false || $identity === null || $identity['mtime'] >= $cutoff) {
            return false;
        }
        $path = $this->sessionFilePath($id);
        $handle = \EasyChat\Library\SafeRuntime::call(function () use ($path) {
            return fopen($path, 'r+b');
        }, false);
        if ($handle === false) {
            return false;
        }
        if (!flock($handle, LOCK_EX | LOCK_NB)) {
            fclose($handle);
            return false;
        }

        clearstatcache(true, $path);
        $path_stat = lstat($path);
        $handle_stat = fstat($handle);
        $safe = $path_stat !== false
            && $handle_stat !== false
            && (int)$path_stat['dev'] === $identity['dev']
            && (int)$path_stat['ino'] === $identity['ino']
            && (int)$handle_stat['dev'] === $identity['dev']
            && (int)$handle_stat['ino'] === $identity['ino']
            && (int)$path_stat['nlink'] === 1
            && ($path_stat['mode'] & 0170000) === 0100000
            && ($path_stat['mode'] & 0777) === 0600
            && (int)$path_stat['uid'] === $this->directory_identity['uid']
            && (int)$path_stat['mtime'] < $cutoff;
        $deleted = $safe && \EasyChat\Library\SafeRuntime::call(function () use ($path) {
            return unlink($path);
        }, false);
        flock($handle, LOCK_UN);
        fclose($handle);
        return (bool)$deleted;
    }
}
