Passed
Push — master ( 24daad...73f9da )
by Zaahid
04:25
created

UUStream::getSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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
use GuzzleHttp\Psr7\BufferStream;
12
use RuntimeException;
13
14
/**
15
 * GuzzleHttp\Psr7 stream decoder extension for UU-Encoded streams.
16
 *
17
 * The size of the underlying stream and the position of bytes can't be
18
 * determined because the number of encoded bytes is indeterminate without
19
 * reading the entire stream.
20
 *
21
 * @author Zaahid Bateson
22
 */
23
class UUStream implements StreamInterface
24
{
25
    use StreamDecoratorTrait;
26
27
    /**
28
     * @var string name of the UUEncoded file
29
     */
30
    protected $filename = null;
31
32
    /**
33
     * @var BufferStream of read and decoded bytes
34
     */
35
    private $buffer;
36
37
    /**
38
     * @var string remainder of write operation if the bytes didn't align to 3
39
     *      bytes
40
     */
41
    private $remainder = '';
42
43
    /**
44
     * @var int read/write position
45
     */
46
    private $position = 0;
47
48
    /**
49
     * @var boolean set to true when 'write' is called
50
     */
51
    private $isWriting = false;
52
53
    /**
54
     * @param StreamInterface $stream Stream to decorate
55
     * @param string optional file name
0 ignored issues
show
Bug introduced by
The type ZBateson\StreamDecorators\optional was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
56
     */
57 12
    public function __construct(StreamInterface $stream, $filename = null)
58
    {
59 12
        $this->stream = $stream;
60 12
        $this->filename = $filename;
61 12
        $this->buffer = new BufferStream();
62 12
    }
63
64
    /**
65
     * Overridden to return the position in the target encoding.
66
     *
67
     * @return int
68
     */
69 3
    public function tell()
70
    {
71 3
        return $this->position;
72
    }
73
74
    /**
75
     * Returns null, getSize isn't supported
76
     *
77
     * @return null
78
     */
79 1
    public function getSize()
80
    {
81 1
        return null;
82
    }
83
84
    /**
85
     * Not supported.
86
     *
87
     * @param int $offset
88
     * @param int $whence
89
     * @throws RuntimeException
90
     */
91 1
    public function seek($offset, $whence = SEEK_SET)
92
    {
93 1
        throw new RuntimeException('Cannot seek a UUStream');
94
    }
95
96
    /**
97
     * Overridden to return false
98
     *
99
     * @return boolean
100
     */
101 1
    public function isSeekable()
102
    {
103 1
        return false;
104
    }
105
106
    /**
107
     * Finds the next end-of-line character to ensure a line isn't broken up
108
     * while buffering.
109
     *
110
     * @return string
111
     */
112 10
    private function readToEndOfLine($length)
113
    {
114 10
        $str = $this->stream->read($length);
115 10
        if ($str === false || $str === '') {
116 10
            return $str;
117
        }
118 10
        while (substr($str, -1) !== "\n") {
119 1
            $chr = $this->stream->read(1);
120 1
            if ($chr === false || $chr === '') {
121 1
                break;
122
            }
123
            $str .= $chr;
124
        }
125 10
        return $str;
126
    }
127
128
    /**
129
     * Removes invalid characters from a uuencoded string, and 'BEGIN' and 'END'
130
     * line headers and footers from the passed string before returning it.
131
     *
132
     * @param string $str
133
     * @return string
134
     */
135 10
    private function filterAndDecode($str)
136
    {
137 10
        $ret = str_replace("\r", '', $str);
138 10
        $ret = preg_replace('/[^\x21-\xf5`\n]/', '`', $ret);
139 10
        if ($this->position === 0) {
140 10
            $matches = [];
141 10
            if (preg_match('/^\s*begin\s+[^\s+]\s+([^\r\n]+)\s*$/im', $ret, $matches)) {
142
                $this->filename = $matches[1];
143
            }
144 10
            $ret = preg_replace('/^\s*begin[^\r\n]+\s*$/im', '', $ret);
145
        } else {
146
            $ret = preg_replace('/^\s*end\s*$/im', '', $ret);
147
        }
148 10
        return convert_uudecode(trim($ret));
149
    }
150
151
    /**
152
     * Buffers bytes into $this->buffer, removing uuencoding headers and footers
153
     * and decoding them.
154
     */
155 10
    private function fillBuffer($length)
156
    {
157
        // 5040 = 63 * 80, seems to be good balance for buffering in benchmarks
158
        // testing with a simple 'if ($length < x)' and calculating a better
159
        // size reduces speeds by up to 4x
160 10
        while ($this->buffer->getSize() < $length) {
161 10
            $read = $this->readToEndOfLine(5040);
162 10
            if ($read === false || $read === '') {
163 10
                break;
164
            }
165 10
            $this->buffer->write($this->filterAndDecode($read));
166
        }
167 10
    }
168
169
    /**
170
     * Returns true if the end of stream has been reached.
171
     *
172
     * @return boolean
173
     */
174 10
    public function eof()
175
    {
176 10
        return ($this->buffer->eof() && $this->stream->eof());
177
    }
178
179
    /**
180
     * Attempts to read $length bytes after decoding them, and returns them.
181
     *
182
     * @param int $length
183
     * @return string
184
     */
185 10
    public function read($length)
186
    {
187
        // let Guzzle decide what to do.
188 10
        if ($length <= 0 || $this->eof()) {
189
            return $this->stream->read($length);
190
        }
191 10
        $this->fillBuffer($length);
192 10
        $read = $this->buffer->read($length);
193 10
        $this->position += strlen($read);
194 10
        return $read;
195
    }
196
197
    /**
198
     * Writes the 'begin' UU header line.
199
     */
200 2
    private function writeUUHeader()
201
    {
202 2
        $filename = (empty($this->filename)) ? 'null' : $this->filename;
203 2
        $this->stream->write("begin 666 $filename");
204 2
    }
205
206
    /**
207
     * Writes the '`' and 'end' UU footer lines.
208
     */
209 2
    private function writeUUFooter()
210
    {
211 2
        $this->stream->write("\r\n`\r\nend\r\n");
212 2
        $this->footerWritten = true;
0 ignored issues
show
Bug Best Practice introduced by
The property footerWritten does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
213 2
    }
214
215
    /**
216
     * Writes the passed bytes to the underlying stream after encoding them.
217
     *
218
     * @param string $bytes
219
     */
220 2
    private function writeEncoded($bytes)
221
    {
222 2
        $encoded = preg_replace('/\r\n|\r|\n/', "\r\n", rtrim(convert_uuencode($bytes)));
223
        // removes ending '`' line
224 2
        $this->stream->write("\r\n" . rtrim(substr($encoded, 0, -1)));
225 2
    }
226
227
    /**
228
     * Prepends any existing remainder to the passed string, then checks if the
229
     * string fits into a uuencoded line, and removes and keeps any remainder
230
     * from the string to write.  Full lines ready for writing are returned.
231
     * 
232
     * @param string $string
233
     * @return string
234
     */
235 2
    private function handleRemainder($string)
236
    {
237 2
        $write = $this->remainder . $string;
238 2
        $nRem = strlen($write) % 45;
239 2
        $this->remainder = '';
240 2
        if ($nRem !== 0) {
241 2
            $this->remainder = substr($write, -$nRem);
242 2
            $write = substr($write, 0, -$nRem);
243
        }
244 2
        return $write;
245
    }
246
247
    /**
248
     * Writes the passed string to the underlying stream after encoding it.
249
     *
250
     * Note that reading and writing to the same stream without rewinding is not
251
     * supported.
252
     *
253
     * Also note that some bytes may not be written until close or detach are
254
     * called.  This happens if written data doesn't align to a complete
255
     * uuencoded 'line' of 45 bytes.  In addition, the UU footer is only written
256
     * when closing or detaching as well.
257
     *
258
     * @param string $string
259
     * @return int the number of bytes written
260
     */
261 2
    public function write($string)
262
    {
263 2
        $this->isWriting = true;
264 2
        if ($this->position === 0) {
265 2
            $this->writeUUHeader();
266
        }
267 2
        $write = $this->handleRemainder($string);
268 2
        if ($write !== '') {
269 2
            $this->writeEncoded($write);
270
        }
271 2
        $written = strlen($string);
272 2
        $this->position += $written;
273 2
        return $written;
274
    }
275
276
    /**
277
     * Returns the filename set in the UUEncoded header (or null)
278
     *
279
     * @return string
280
     */
281
    public function getFilename()
282
    {
283
        return $this->filename;
284
    }
285
286
    /**
287
     * Sets the UUEncoded header file name written in the 'begin' header line.
288
     *
289
     * @param string $filename
290
     */
291
    public function setFilename($filename)
292
    {
293
        $this->filename = $filename;
294
    }
295
296
    /**
297
     * Writes out any remaining bytes and the UU footer.
298
     */
299 2
    private function beforeClose()
300
    {
301 2
        if (!$this->isWriting) {
302 1
            return;
303
        }
304 2
        if ($this->remainder !== '') {
305 2
            $this->writeEncoded($this->remainder);
306
        }
307 2
        $this->remainder = '';
308 2
        $this->isWriting = false;
309 2
        $this->writeUUFooter();
310 2
    }
311
312
    /**
313
     * Writes any remaining bytes out followed by the uu-encoded footer, then
314
     * closes the stream.
315
     */
316 2
    public function close()
317
    {
318 2
        $this->beforeClose();
319 2
        $this->stream->close();
320 2
    }
321
322
    /**
323
     * Writes any remaining bytes out followed by the uu-encoded footer, then
324
     * detaches the stream.
325
     */
326
    public function detach()
327
    {
328
        $this->beforeClose();
329
        $this->stream->detach();
330
    }
331
}
332