Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   1  <?php
   2  
   3  /**
   4   * This file is part of FPDI
   5   *
   6   * @package   setasign\Fpdi
   7   * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com)
   8   * @license   http://opensource.org/licenses/mit-license The MIT License
   9   */
  10  
  11  namespace setasign\Fpdi\PdfParser;
  12  
  13  /**
  14   * A stream reader class
  15   */
  16  class StreamReader
  17  {
  18      /**
  19       * Creates a stream reader instance by a string value.
  20       *
  21       * @param string $content
  22       * @param int $maxMemory
  23       * @return StreamReader
  24       */
  25      public static function createByString($content, $maxMemory = 2097152)
  26      {
  27          $h = \fopen('php://temp/maxmemory:' . ((int) $maxMemory), 'r+b');
  28          \fwrite($h, $content);
  29          \rewind($h);
  30  
  31          return new self($h, true);
  32      }
  33  
  34      /**
  35       * Creates a stream reader instance by a filename.
  36       *
  37       * @param string $filename
  38       * @return StreamReader
  39       */
  40      public static function createByFile($filename)
  41      {
  42          $h = \fopen($filename, 'rb');
  43          return new self($h, true);
  44      }
  45  
  46      /**
  47       * Defines whether the stream should be closed when the stream reader instance is deconstructed or not.
  48       *
  49       * @var bool
  50       */
  51      protected $closeStream;
  52  
  53      /**
  54       * The stream resource.
  55       *
  56       * @var resource
  57       */
  58      protected $stream;
  59  
  60      /**
  61       * The byte-offset position in the stream.
  62       *
  63       * @var int
  64       */
  65      protected $position;
  66  
  67      /**
  68       * The byte-offset position in the buffer.
  69       *
  70       * @var int
  71       */
  72      protected $offset;
  73  
  74      /**
  75       * The buffer length.
  76       *
  77       * @var int
  78       */
  79      protected $bufferLength;
  80  
  81      /**
  82       * The total length of the stream.
  83       *
  84       * @var int
  85       */
  86      protected $totalLength;
  87  
  88      /**
  89       * The buffer.
  90       *
  91       * @var string
  92       */
  93      protected $buffer;
  94  
  95      /**
  96       * StreamReader constructor.
  97       *
  98       * @param resource $stream
  99       * @param bool $closeStream Defines whether to close the stream resource if the instance is destructed or not.
 100       */
 101      public function __construct($stream, $closeStream = false)
 102      {
 103          if (!\is_resource($stream)) {
 104              throw new \InvalidArgumentException(
 105                  'No stream given.'
 106              );
 107          }
 108  
 109          $metaData = \stream_get_meta_data($stream);
 110          if (!$metaData['seekable']) {
 111              throw new \InvalidArgumentException(
 112                  'Given stream is not seekable!'
 113              );
 114          }
 115  
 116          $this->stream = $stream;
 117          $this->closeStream = $closeStream;
 118          $this->reset();
 119      }
 120  
 121      /**
 122       * The destructor.
 123       */
 124      public function __destruct()
 125      {
 126          $this->cleanUp();
 127      }
 128  
 129      /**
 130       * Closes the file handle.
 131       */
 132      public function cleanUp()
 133      {
 134          if ($this->closeStream && is_resource($this->stream)) {
 135              \fclose($this->stream);
 136          }
 137      }
 138  
 139      /**
 140       * Returns the byte length of the buffer.
 141       *
 142       * @param bool $atOffset
 143       * @return int
 144       */
 145      public function getBufferLength($atOffset = false)
 146      {
 147          if ($atOffset === false) {
 148              return $this->bufferLength;
 149          }
 150  
 151          return $this->bufferLength - $this->offset;
 152      }
 153  
 154      /**
 155       * Get the current position in the stream.
 156       *
 157       * @return int
 158       */
 159      public function getPosition()
 160      {
 161          return $this->position;
 162      }
 163  
 164      /**
 165       * Returns the current buffer.
 166       *
 167       * @param bool $atOffset
 168       * @return string
 169       */
 170      public function getBuffer($atOffset = true)
 171      {
 172          if ($atOffset === false) {
 173              return $this->buffer;
 174          }
 175  
 176          $string = \substr($this->buffer, $this->offset);
 177  
 178          return (string) $string;
 179      }
 180  
 181      /**
 182       * Gets a byte at a specific position in the buffer.
 183       *
 184       * If the position is invalid the method will return false.
 185       *
 186       * If the $position parameter is set to null the value of $this->offset will be used.
 187       *
 188       * @param int|null $position
 189       * @return string|bool
 190       */
 191      public function getByte($position = null)
 192      {
 193          $position = (int) ($position !== null ? $position : $this->offset);
 194          if (
 195              $position >= $this->bufferLength
 196              && (!$this->increaseLength() || $position >= $this->bufferLength)
 197          ) {
 198              return false;
 199          }
 200  
 201          return $this->buffer[$position];
 202      }
 203  
 204      /**
 205       * Returns a byte at a specific position, and set the offset to the next byte position.
 206       *
 207       * If the position is invalid the method will return false.
 208       *
 209       * If the $position parameter is set to null the value of $this->offset will be used.
 210       *
 211       * @param int|null $position
 212       * @return string|bool
 213       */
 214      public function readByte($position = null)
 215      {
 216          if ($position !== null) {
 217              $position = (int) $position;
 218              // check if needed bytes are available in the current buffer
 219              if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
 220                  $this->reset($position);
 221                  $offset = $this->offset;
 222              } else {
 223                  $offset = $position - $this->position;
 224              }
 225          } else {
 226              $offset = $this->offset;
 227          }
 228  
 229          if (
 230              $offset >= $this->bufferLength
 231              && ((!$this->increaseLength()) || $offset >= $this->bufferLength)
 232          ) {
 233              return false;
 234          }
 235  
 236          $this->offset = $offset + 1;
 237          return $this->buffer[$offset];
 238      }
 239  
 240      /**
 241       * Read bytes from the current or a specific offset position and set the internal pointer to the next byte.
 242       *
 243       * If the position is invalid the method will return false.
 244       *
 245       * If the $position parameter is set to null the value of $this->offset will be used.
 246       *
 247       * @param int $length
 248       * @param int|null $position
 249       * @return string|false
 250       */
 251      public function readBytes($length, $position = null)
 252      {
 253          $length = (int) $length;
 254          if ($position !== null) {
 255              // check if needed bytes are available in the current buffer
 256              if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
 257                  $this->reset($position, $length);
 258                  $offset = $this->offset;
 259              } else {
 260                  $offset = $position - $this->position;
 261              }
 262          } else {
 263              $offset = $this->offset;
 264          }
 265  
 266          if (
 267              ($offset + $length) > $this->bufferLength
 268              && ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength)
 269          ) {
 270              return false;
 271          }
 272  
 273          $bytes = \substr($this->buffer, $offset, $length);
 274          $this->offset = $offset + $length;
 275  
 276          return $bytes;
 277      }
 278  
 279      /**
 280       * Read a line from the current position.
 281       *
 282       * @param int $length
 283       * @return string|bool
 284       */
 285      public function readLine($length = 1024)
 286      {
 287          if ($this->ensureContent() === false) {
 288              return false;
 289          }
 290  
 291          $line = '';
 292          while ($this->ensureContent()) {
 293              $char = $this->readByte();
 294  
 295              if ($char === "\n") {
 296                  break;
 297              }
 298  
 299              if ($char === "\r") {
 300                  if ($this->getByte() === "\n") {
 301                      $this->addOffset(1);
 302                  }
 303                  break;
 304              }
 305  
 306              $line .= $char;
 307  
 308              if (\strlen($line) >= $length) {
 309                  break;
 310              }
 311          }
 312  
 313          return $line;
 314      }
 315  
 316      /**
 317       * Set the offset position in the current buffer.
 318       *
 319       * @param int $offset
 320       */
 321      public function setOffset($offset)
 322      {
 323          if ($offset > $this->bufferLength || $offset < 0) {
 324              throw new \OutOfRangeException(
 325                  \sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength)
 326              );
 327          }
 328  
 329          $this->offset = (int) $offset;
 330      }
 331  
 332      /**
 333       * Returns the current offset in the current buffer.
 334       *
 335       * @return int
 336       */
 337      public function getOffset()
 338      {
 339          return $this->offset;
 340      }
 341  
 342      /**
 343       * Add an offset to the current offset.
 344       *
 345       * @param int $offset
 346       */
 347      public function addOffset($offset)
 348      {
 349          $this->setOffset($this->offset + $offset);
 350      }
 351  
 352      /**
 353       * Make sure that there is at least one character beyond the current offset in the buffer.
 354       *
 355       * @return bool
 356       */
 357      public function ensureContent()
 358      {
 359          while ($this->offset >= $this->bufferLength) {
 360              if (!$this->increaseLength()) {
 361                  return false;
 362              }
 363          }
 364          return true;
 365      }
 366  
 367      /**
 368       * Returns the stream.
 369       *
 370       * @return resource
 371       */
 372      public function getStream()
 373      {
 374          return $this->stream;
 375      }
 376  
 377      /**
 378       * Gets the total available length.
 379       *
 380       * @return int
 381       */
 382      public function getTotalLength()
 383      {
 384          if ($this->totalLength === null) {
 385              $stat = \fstat($this->stream);
 386              $this->totalLength = $stat['size'];
 387          }
 388  
 389          return $this->totalLength;
 390      }
 391  
 392      /**
 393       * Resets the buffer to a position and re-read the buffer with the given length.
 394       *
 395       * If the $pos parameter is negative the start buffer position will be the $pos'th position from
 396       * the end of the file.
 397       *
 398       * If the $pos parameter is negative and the absolute value is bigger then the totalLength of
 399       * the file $pos will set to zero.
 400       *
 401       * @param int|null $pos Start position of the new buffer
 402       * @param int $length Length of the new buffer. Mustn't be negative
 403       */
 404      public function reset($pos = 0, $length = 200)
 405      {
 406          if ($pos === null) {
 407              $pos = $this->position + $this->offset;
 408          } elseif ($pos < 0) {
 409              $pos = \max(0, $this->getTotalLength() + $pos);
 410          }
 411  
 412          \fseek($this->stream, $pos);
 413  
 414          $this->position = $pos;
 415          $this->buffer = $length > 0 ? \fread($this->stream, $length) : '';
 416          $this->bufferLength = \strlen($this->buffer);
 417          $this->offset = 0;
 418  
 419          // If a stream wrapper is in use it is possible that
 420          // length values > 8096 will be ignored, so use the
 421          // increaseLength()-method to correct that behavior
 422          if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) {
 423              // increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer
 424              $this->buffer = \substr($this->buffer, 0, $length);
 425              $this->bufferLength = \strlen($this->buffer);
 426          }
 427      }
 428  
 429      /**
 430       * Ensures bytes in the buffer with a specific length and location in the file.
 431       *
 432       * @param int $pos
 433       * @param int $length
 434       * @see reset()
 435       */
 436      public function ensure($pos, $length)
 437      {
 438          if (
 439              $pos >= $this->position
 440              && $pos < ($this->position + $this->bufferLength)
 441              && ($this->position + $this->bufferLength) >= ($pos + $length)
 442          ) {
 443              $this->offset = $pos - $this->position;
 444          } else {
 445              $this->reset($pos, $length);
 446          }
 447      }
 448  
 449      /**
 450       * Forcefully read more data into the buffer.
 451       *
 452       * @param int $minLength
 453       * @return bool Returns false if the stream reaches the end
 454       */
 455      public function increaseLength($minLength = 100)
 456      {
 457          $length = \max($minLength, 100);
 458  
 459          if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) {
 460              return false;
 461          }
 462  
 463          $newLength = $this->bufferLength + $length;
 464          do {
 465              $this->buffer .= \fread($this->stream, $newLength - $this->bufferLength);
 466              $this->bufferLength = \strlen($this->buffer);
 467          } while (($this->bufferLength !== $newLength) && !\feof($this->stream));
 468  
 469          return true;
 470      }
 471  }