Passed
Push — master ( bd124d...a4499c )
by Zaahid
03:53
created

SeekingLimitStream::eof()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the ZBateson\StreamDecorators project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
namespace ZBateson\StreamDecorators;
8
9
use Psr\Http\Message\StreamInterface;
10
use GuzzleHttp\Psr7\StreamDecoratorTrait;
11
12
/**
13
 * Maintains an internal 'read' position, and seeks to it before reading, then
14
 * seeks back to the original position of the underlying stream after reading if
15
 * the attached stream supports seeking.
16
 *
17
 * Although based on LimitStream, it's not inherited from it since $offset and
18
 * $limit are set to private on LimitStream, and most other functions are re-
19
 * implemented anyway.  This also decouples the implementation from upstream
20
 * changes.
21
 *
22
 * @author Zaahid Bateson
23
 */
24
class SeekingLimitStream implements StreamInterface
25
{
26
    use StreamDecoratorTrait;
27
28
    /** @var int Offset to start reading from */
29
    private $offset;
30
31
    /** @var int Limit the number of bytes that can be read */
32
    private $limit;
33
34
    /**
35
     * @var int Number of bytes written, and importantly, if non-zero, writes a
36
     *      final $lineEnding on close (and so maintained instead of using
37
     *      tell() directly)
38
     */
39
    private $position = 0;
40
41
    /**
42
     * @param StreamInterface $stream Stream to wrap
43
     * @param int             $limit  Total number of bytes to allow to be read
44
     *                                from the stream. Pass -1 for no limit.
45
     * @param int             $offset Position to seek to before reading (only
46
     *                                works on seekable streams).
47
     */
48 12
    public function __construct(
49
        StreamInterface $stream,
50
        $limit = -1,
51
        $offset = 0
52
    ) {
53 12
        $this->stream = $stream;
54 12
        $this->setLimit($limit);
55 12
        $this->setOffset($offset);
56 12
    }
57
58
    /**
59
     * Returns the current relative read position of this stream subset.
60
     * 
61
     * @return int
62
     */
63 2
    public function tell()
64
    {
65 2
        return $this->position;
66
    }
67
68
    /**
69
     * Returns the size of the limited subset of data, or null if the wrapped
70
     * stream returns null for getSize.
71
     *
72
     * @return int|null
73
     */
74 6
    public function getSize()
75
    {
76 6
        $size = $this->stream->getSize();
77 6
        if ($size === null) {
78
            // this shouldn't happen on a seekable stream I don't think...
79
            $pos = $this->stream->tell();
80
            $this->stream->seek(0, SEEK_END);
81
            $size = $this->stream->tell();
82
            $this->stream->seek($pos);
83
        }
84 6
        if ($this->limit === -1) {
85 3
            return $size - $this->offset;
86
        } else {
87 3
            return min([$this->limit, $size - $this->offset]);
88
        }
89
    }
90
91
    /**
92
     * Returns true if the current read position is at the end of the limited
93
     * stream
94
     * 
95
     * @return boolean
96
     */
97 8
    public function eof()
98
    {
99 8
        $size = $this->limit;
100 8
        if ($size === -1) {
101 2
            $size = $this->getSize();
102
        }
103 8
        return ($this->position >= $size);
104
    }
105
106
    /**
107
     * Ensures the seek position specified is within the stream's bounds, and
108
     * sets the internal position pointer (doesn't actually seek).
109
     * 
110
     * @param int $pos
111
     */
112 1
    private function doSeek($pos)
113
    {
114 1
        if ($this->limit !== -1) {
115 1
            $pos = min([$pos, $this->limit]);
116
        }
117 1
        $this->position = max([0, $pos]);
118 1
    }
119
120
    /**
121
     * Seeks to the passed position within the confines of the limited stream's
122
     * bounds.
123
     *
124
     * For SeekingLimitStream, no actual seek is performed on the underlying
125
     * wrapped stream.  Instead, an internal pointer is set, and the stream is
126
     * 'seeked' on read operations
127
     *
128
     * @param int $offset
129
     * @param int $whence
130
     */
131 1
    public function seek($offset, $whence = SEEK_SET)
132
    {
133 1
        $pos = $offset;
134
        switch ($whence) {
135 1
            case SEEK_CUR:
136 1
                $pos = $this->position + $offset;
137 1
                break;
138 1
            case SEEK_END:
139 1
                $pos = $this->limit + $offset;
140 1
                break;
141
            default:
142 1
                break;
143
        }
144 1
        $this->doSeek($pos);
145 1
    }
146
147
    /**
148
     * Sets the offset to start reading from the wrapped stream.
149
     *
150
     * @param int $offset
151
     * @throws \RuntimeException if the stream cannot be seeked.
152
     */
153 12
    public function setOffset($offset)
154
    {
155 12
        $this->offset = $offset;
156 12
        $this->position = 0;
157 12
    }
158
159
    /**
160
     * Sets the length of the stream to the passed $limit.
161
     *
162
     * @param int $limit
163
     */
164 12
    public function setLimit($limit)
165
    {
166 12
        $this->limit = $limit;
167 12
    }
168
169
    /**
170
     * Seeks to the current position and reads up to $length bytes, or less if
171
     * it would result in reading past $this->limit
172
     *
173
     * @param int $length
174
     * @return string
175
     */
176 8
    public function seekAndRead($length)
177
    {
178 8
        $this->stream->seek($this->offset + $this->position);
179 8
        if ($this->limit !== -1) {
180 6
            $length = min($length, $this->limit - $this->position);
181 6
            if ($length <= 0) {
182
                return '';
183
            }
184
        }
185 8
        return $this->stream->read($length);
186
    }
187
188
    /**
189
     * Reads from the underlying stream after seeking to the position within the
190
     * bounds set for this limited stream.  After reading, the wrapped stream is
191
     * 'seeked' back to its position prior to the call to read().
192
     *
193
     * @param int $length
194
     * @return string
195
     */
196 8
    public function read($length)
197
    {
198 8
        $pos = $this->stream->tell();
199 8
        $ret = $this->seekAndRead($length);
200 8
        $this->position += strlen($ret);
201 8
        $this->stream->seek($pos);
202 8
        if ($this->limit !== -1 && $this->position > $this->limit) {
203
            $ret = substr($ret, 0, -($this->position - $this->limit));
204
            $this->position = $this->limit;
205
        }
206 8
        return $ret;
207
    }
208
}
209