Passed
Push — master ( 783b03...a6142d )
by Zaahid
02:20 queued 13s
created

src/Base64Stream.php (2 issues)

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\BufferStream;
11
use GuzzleHttp\Psr7\StreamDecoratorTrait;
12
use Psr\Http\Message\StreamInterface;
13
use RuntimeException;
14
15
/**
16
 * GuzzleHttp\Psr7 stream decoder extension for base64 streams.
17
 *
18
 * Note that it's expected the underlying stream will only contain valid base64
19
 * characters (normally the stream should be wrapped in a
20
 * PregReplaceFilterStream to filter out non-base64 characters for reading).
21
 *
22
 * ```
23
 * $f = fopen(...);
24
 * $stream = new Base64Stream(new PregReplaceFilterStream(
25
 *      Psr7\Utils::streamFor($f), '/[^a-zA-Z0-9\/\+=]/', ''
26
 * ));
27
 * //...
28
 * ```
29
 *
30
 * For writing, a ChunkSplitStream could come in handy so the output is split
31
 * into lines:
32
 *
33
 * ```
34
 * $f = fopen(...);
35
 * $stream = new Base64Stream(new ChunkSplitStream(new PregReplaceFilterStream(
36
 *      Psr7\Utils::streamFor($f), '/[^a-zA-Z0-9\/\+=]/', ''
37
 * )));
38
 * //...
39
 * ```
40
 *
41
 * @author Zaahid Bateson
42
 */
43
class Base64Stream implements StreamInterface
44
{
45
    use StreamDecoratorTrait;
46
47
    /**
48
     * @var BufferStream buffered bytes
49
     */
50
    private $buffer;
51
52
    /**
53
     * @var string remainder of write operation if the bytes didn't align to 3
54
     *      bytes
55
     */
56
    private $remainder = '';
57
58
    /**
59
     * @var int current number of read/written bytes (for tell())
60
     */
61
    private $position = 0;
62
63
    /**
64
     * @var StreamInterface $stream
65
     */
66
    private $stream;
67
68 9
    public function __construct(StreamInterface $stream)
69
    {
70 9
        $this->stream = $stream;
71 9
        $this->buffer = new BufferStream();
72 9
    }
73
74
    /**
75
     * Returns the current position of the file read/write pointer
76
     */
77 1
    public function tell() : int
78
    {
79 1
        return $this->position;
80
    }
81
82
    /**
83
     * Returns null, getSize isn't supported
84
     *
85
     * @return null
86
     */
87 3
    public function getSize() : ?int
88
    {
89 3
        return null;
90
    }
91
92
    /**
93
     * Not implemented (yet).
94
     *
95
     * Seek position can be calculated.
96
     *
97
     * @param int $offset
98
     * @param int $whence
99
     * @throws RuntimeException
100
     */
101 1
    public function seek($offset, $whence = SEEK_SET) : void
102
    {
103 1
        throw new RuntimeException('Cannot seek a Base64Stream');
104
    }
105
106
    /**
107
     * Overridden to return false
108
     */
109 1
    public function isSeekable() : bool
110
    {
111 1
        return false;
112
    }
113
114
    /**
115
     * Returns true if the end of stream has been reached.
116
     */
117 7
    public function eof() : bool
118
    {
119 7
        return ($this->buffer->eof() && $this->stream->eof());
120
    }
121
122
    /**
123
     * Fills the internal byte buffer after reading and decoding data from the
124
     * underlying stream.
125
     *
126
     * Note that it's expected the underlying stream will only contain valid
127
     * base64 characters (normally the stream should be wrapped in a
128
     * PregReplaceFilterStream to filter out non-base64 characters).
129
     */
130 7
    private function fillBuffer(int $length) : void
131
    {
132 7
        $fill = 8192;
133 7
        while ($this->buffer->getSize() < $length) {
134 7
            $read = $this->stream->read($fill);
135 7
            if ($read === '') {
136 6
                break;
137
            }
138 7
            $this->buffer->write(\base64_decode($read));
139
        }
140 7
    }
141
142
    /**
143
     * Attempts to read $length bytes after decoding them, and returns them.
144
     *
145
     * Note that reading and writing to the same stream may result in wrongly
146
     * encoded data and is not supported.
147
     *
148
     * @param int $length
149
     * @return string
150
     */
151 7
    public function read($length) : string
152
    {
153
        // let Guzzle decide what to do.
154 7
        if ($length <= 0 || $this->eof()) {
155 2
            return $this->stream->read($length);
156
        }
157 7
        $this->fillBuffer($length);
158 7
        $ret = $this->buffer->read($length);
159 7
        $this->position += \strlen($ret);
160 7
        return $ret;
161
    }
162
163
    /**
164
     * Writes the passed string to the underlying stream after encoding it to
165
     * base64.
166
     *
167
     * Base64Stream::close or detach must be called.  Failing to do so may
168
     * result in 1-2 bytes missing from the end of the stream if there's a
169
     * remainder.  Note that the default Stream destructor calls close as well.
170
     *
171
     * Note that reading and writing to the same stream may result in wrongly
172
     * encoded data and is not supported.
173
     *
174
     * @param string $string
0 ignored issues
show
Tag value indented incorrectly; expected 2 spaces but found 1
Loading history...
175
     * @return int the number of bytes written
0 ignored issues
show
Tag cannot be grouped with parameter tags in a doc comment
Loading history...
176
     */
177 2
    public function write($string) : int
178
    {
179 2
        $bytes = $this->remainder . $string;
180 2
        $len = \strlen($bytes);
181 2
        if (($len % 3) !== 0) {
182 2
            $this->remainder = \substr($bytes, -($len % 3));
183 2
            $bytes = \substr($bytes, 0, $len - ($len % 3));
184
        } else {
185 2
            $this->remainder = '';
186
        }
187 2
        $this->stream->write(\base64_encode($bytes));
188 2
        $written = \strlen($string);
189 2
        $this->position += $len;
190 2
        return $written;
191
    }
192
193
    /**
194
     * Writes out any remaining bytes at the end of the stream and closes.
195
     */
196 2
    private function beforeClose() : void
197
    {
198 2
        if ($this->isWritable() && $this->remainder !== '') {
199 2
            $this->stream->write(\base64_encode($this->remainder));
200 2
            $this->remainder = '';
201
        }
202 2
    }
203
204
    /**
205
     * @inheritDoc
206
     */
207 1
    public function close() : void
208
    {
209 1
        $this->beforeClose();
210 1
        $this->stream->close();
211 1
    }
212
213
    /**
214
     * @inheritDoc
215
     */
216 1
    public function detach()
217
    {
218 1
        $this->beforeClose();
219 1
        $this->stream->detach();
220
221 1
        return null;
222
    }
223
}
224