1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use Psr\Http\Message\StreamInterface; 8 9/** 10 * Decorator used to return only a subset of a stream. 11 */ 12final class LimitStream implements StreamInterface 13{ 14 use StreamDecoratorTrait; 15 16 /** @var int Offset to start reading from */ 17 private $offset; 18 19 /** @var int Limit the number of bytes that can be read */ 20 private $limit; 21 22 /** @var StreamInterface */ 23 private $stream; 24 25 /** 26 * @param StreamInterface $stream Stream to wrap 27 * @param int $limit Total number of bytes to allow to be read 28 * from the stream. Pass -1 for no limit. 29 * @param int $offset Position to seek to before reading (only 30 * works on seekable streams). 31 */ 32 public function __construct( 33 StreamInterface $stream, 34 int $limit = -1, 35 int $offset = 0 36 ) { 37 $this->stream = $stream; 38 $this->setLimit($limit); 39 $this->setOffset($offset); 40 } 41 42 public function eof(): bool 43 { 44 // Always return true if the underlying stream is EOF 45 if ($this->stream->eof()) { 46 return true; 47 } 48 49 // No limit and the underlying stream is not at EOF 50 if ($this->limit === -1) { 51 return false; 52 } 53 54 return $this->stream->tell() >= $this->offset + $this->limit; 55 } 56 57 /** 58 * Returns the size of the limited subset of data 59 */ 60 public function getSize(): ?int 61 { 62 if (null === ($length = $this->stream->getSize())) { 63 return null; 64 } elseif ($this->limit === -1) { 65 return $length - $this->offset; 66 } else { 67 return min($this->limit, $length - $this->offset); 68 } 69 } 70 71 /** 72 * Allow for a bounded seek on the read limited stream 73 */ 74 public function seek($offset, $whence = SEEK_SET): void 75 { 76 if ($whence !== SEEK_SET || $offset < 0) { 77 throw new \RuntimeException(sprintf( 78 'Cannot seek to offset %s with whence %s', 79 $offset, 80 $whence 81 )); 82 } 83 84 $offset += $this->offset; 85 86 if ($this->limit !== -1) { 87 if ($offset > $this->offset + $this->limit) { 88 $offset = $this->offset + $this->limit; 89 } 90 } 91 92 $this->stream->seek($offset); 93 } 94 95 /** 96 * Give a relative tell() 97 */ 98 public function tell(): int 99 { 100 return $this->stream->tell() - $this->offset; 101 } 102 103 /** 104 * Set the offset to start limiting from 105 * 106 * @param int $offset Offset to seek to and begin byte limiting from 107 * 108 * @throws \RuntimeException if the stream cannot be seeked. 109 */ 110 public function setOffset(int $offset): void 111 { 112 $current = $this->stream->tell(); 113 114 if ($current !== $offset) { 115 // If the stream cannot seek to the offset position, then read to it 116 if ($this->stream->isSeekable()) { 117 $this->stream->seek($offset); 118 } elseif ($current > $offset) { 119 throw new \RuntimeException("Could not seek to stream offset $offset"); 120 } else { 121 $this->stream->read($offset - $current); 122 } 123 } 124 125 $this->offset = $offset; 126 } 127 128 /** 129 * Set the limit of bytes that the decorator allows to be read from the 130 * stream. 131 * 132 * @param int $limit Number of bytes to allow to be read from the stream. 133 * Use -1 for no limit. 134 */ 135 public function setLimit(int $limit): void 136 { 137 $this->limit = $limit; 138 } 139 140 public function read($length): string 141 { 142 if ($this->limit === -1) { 143 return $this->stream->read($length); 144 } 145 146 // Check if the current position is less than the total allowed 147 // bytes + original offset 148 $remaining = ($this->offset + $this->limit) - $this->stream->tell(); 149 if ($remaining > 0) { 150 // Only return the amount of requested data, ensuring that the byte 151 // limit is not exceeded 152 return $this->stream->read(min($remaining, $length)); 153 } 154 155 return ''; 156 } 157} 158