<?php require_once __DIR__ . '/StreamReader.php'; class BufferedReader implements StreamReader { private const int CHUNK_SIZE = 65536; private mixed $handle; private string $buffer = ''; private int $bufferPos = 0; private int $bufferLen = 0; private string $wBuffer = ''; private int $wBufferLen = 0; private readonly bool $writable; public function __construct( string $path, string $mode = 'rb' ) { $this->handle = @\fopen( $path, $mode ); $this->writable = $mode !== 'rb'; } public function __destruct() { if( $this->handle !== false ) { $this->flushWrites(); \fclose( $this->handle ); $this->handle = false; } } public function isOpen(): bool { return $this->handle !== false; } public function read( int $length ): string { $available = $this->bufferLen - $this->bufferPos; if( $available >= $length ) { $result = \substr( $this->buffer, $this->bufferPos, $length ); $this->bufferPos += $length; if( $this->bufferPos >= $this->bufferLen ) { $this->clearReadBuffer(); } return $result; } if( $this->bufferPos > 0 ) { $this->buffer = $available > 0 ? \substr( $this->buffer, $this->bufferPos ) : ''; $this->bufferLen = $available; $this->bufferPos = 0; } if( $this->handle !== false && !\feof( $this->handle ) ) { $chunk = \fread( $this->handle, \max( $length - $available, self::CHUNK_SIZE ) ); if( $chunk !== false && $chunk !== '' ) { $this->buffer .= $chunk; $this->bufferLen += \strlen( $chunk ); $available = $this->bufferLen; } } $take = \min( $available, $length ); $result = $take > 0 ? \substr( $this->buffer, $this->bufferPos, $take ) : ''; if( $take > 0 ) { $this->bufferPos += $take; if( $this->bufferPos >= $this->bufferLen ) { $this->clearReadBuffer(); } } return $result; } public function write( string $data ): bool { if( !$this->writable || $this->handle === false ) { return false; } $this->clearReadBuffer(); $this->wBuffer .= $data; $this->wBufferLen += \strlen( $data ); if( $this->wBufferLen < self::CHUNK_SIZE ) { return true; } return $this->flushWrites(); } public function seek( int $offset, int $whence = SEEK_SET ): bool { $current = $this->tell(); $target = $whence === SEEK_CUR ? $current + $offset : $offset; $bufSt = $current - $this->bufferPos; $bufEn = $bufSt + $this->bufferLen; $success = false; if( $whence !== SEEK_END && $target >= $bufSt && $target <= $bufEn ) { $this->bufferPos = $target - $bufSt; $success = true; } else { $seekTgt = $whence === SEEK_END ? $offset : $target; $seekWh = $whence === SEEK_END ? SEEK_END : SEEK_SET; $success = $this->handle !== false && \fseek( $this->handle, $seekTgt, $seekWh ) === 0; if( $success ) { $this->clearReadBuffer(); } } return $success; } public function tell(): int { return $this->handle !== false ? (int)\ftell( $this->handle ) - $this->bufferLen + $this->bufferPos : 0; } public function eof(): bool { return $this->bufferPos >= $this->bufferLen && ($this->handle === false || \feof( $this->handle )); } public function rewind(): void { if( $this->handle !== false ) { $this->flushWrites(); \rewind( $this->handle ); $this->clearReadBuffer(); } } private function clearReadBuffer(): void { $this->buffer = ''; $this->bufferPos = 0; $this->bufferLen = 0; } private function flushWrites(): bool { if( $this->wBufferLen === 0 ) { return true; } $ok = \fwrite( $this->handle, $this->wBuffer ) !== false; $this->wBuffer = ''; $this->wBufferLen = 0; return $ok; } }