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