Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
<?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;
  }
}