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