SeekingLimitStream   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 168
Duplicated Lines 0 %

Test Coverage

Coverage 87.5%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 54
c 4
b 0
f 0
dl 0
loc 168
ccs 49
cts 56
cp 0.875
rs 10
wmc 20

10 Methods

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