UUStream   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Test Coverage

Coverage 86.96%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 36
eloc 81
c 6
b 0
f 0
dl 0
loc 290
ccs 80
cts 92
cp 0.8696
rs 9.52

20 Methods

Rating   Name   Duplication   Size   Complexity  
A tell() 0 3 1
A isSeekable() 0 3 1
A writeUUFooter() 0 3 1
A __construct() 0 5 1
A getFilename() 0 3 1
A eof() 0 3 2
A writeUUHeader() 0 4 2
A setFilename() 0 3 1
A handleRemainder() 0 10 2
A close() 0 4 1
A write() 0 13 3
A beforeClose() 0 11 3
A filterAndDecode() 0 14 3
A getSize() 0 3 1
A seek() 0 3 1
A readToEndOfLine() 0 14 4
A writeEncoded() 0 5 1
A fillBuffer() 0 11 3
A detach() 0 6 1
A read() 0 10 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\BufferStream;
11
use GuzzleHttp\Psr7\StreamDecoratorTrait;
12
use Psr\Http\Message\StreamInterface;
13
use RuntimeException;
14
15
/**
16
 * GuzzleHttp\Psr7 stream decoder extension for UU-Encoded streams.
17
 *
18
 * The size of the underlying stream and the position of bytes can't be
19
 * determined because the number of encoded bytes is indeterminate without
20
 * reading the entire stream.
21
 *
22
 * @author Zaahid Bateson
23
 */
24
class UUStream implements StreamInterface
25
{
26
    use StreamDecoratorTrait;
27
28
    /**
29
     * @var string name of the UUEncoded file
30
     */
31
    protected $filename = null;
32
33
    /**
34
     * @var BufferStream of read and decoded bytes
35
     */
36
    private $buffer;
37
38
    /**
39
     * @var string remainder of write operation if the bytes didn't align to 3
40
     *      bytes
41
     */
42
    private $remainder = '';
43
44
    /**
45
     * @var int read/write position
46
     */
47
    private $position = 0;
48
49
    /**
50
     * @var bool set to true when 'write' is called
51
     */
52
    private $isWriting = false;
53
54
    /**
55
     * @var StreamInterface $stream
56
     */
57
    private $stream;
58
59
    /**
60
     * @param StreamInterface $stream Stream to decorate
61
     * @param string $filename optional file name
62
     */
63 12
    public function __construct(StreamInterface $stream, ?string $filename = null)
64
    {
65 12
        $this->stream = $stream;
66 12
        $this->filename = $filename;
67 12
        $this->buffer = new BufferStream();
68
    }
69
70
    /**
71
     * Overridden to return the position in the target encoding.
72
     */
73 3
    public function tell() : int
74
    {
75 3
        return $this->position;
76
    }
77
78
    /**
79
     * Returns null, getSize isn't supported
80
     *
81
     * @return null
82
     */
83 1
    public function getSize() : ?int
84
    {
85 1
        return null;
86
    }
87
88
    /**
89
     * Not supported.
90
     *
91
     * @param int $offset
92
     * @param int $whence
93
     * @throws RuntimeException
94
     */
95 1
    public function seek($offset, $whence = SEEK_SET) : void
96
    {
97 1
        throw new RuntimeException('Cannot seek a UUStream');
98
    }
99
100
    /**
101
     * Overridden to return false
102
     */
103 1
    public function isSeekable() : bool
104
    {
105 1
        return false;
106
    }
107
108
    /**
109
     * Finds the next end-of-line character to ensure a line isn't broken up
110
     * while buffering.
111
     */
112 10
    private function readToEndOfLine(int $length) : string
113
    {
114 10
        $str = $this->stream->read($length);
115 10
        if ($str === '') {
116 9
            return $str;
117
        }
118 10
        while (\substr($str, -1) !== "\n") {
119 1
            $chr = $this->stream->read(1);
120 1
            if ($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 10
    private function filterAndDecode(string $str) : string
133
    {
134 10
        $ret = \str_replace("\r", '', $str);
135 10
        $ret = \preg_replace('/[^\x21-\xf5`\n]/', '`', $ret);
136 10
        if ($this->position === 0) {
137 10
            $matches = [];
138 10
            if (\preg_match('/^\s*begin\s+[^\s+]\s+([^\r\n]+)\s*$/im', $ret, $matches)) {
139
                $this->filename = $matches[1];
140
            }
141 10
            $ret = \preg_replace('/^\s*begin[^\r\n]+\s*$/im', '', $ret);
142
        } else {
143
            $ret = \preg_replace('/^\s*end\s*$/im', '', $ret);
144
        }
145 10
        return \convert_uudecode(\trim($ret));
146
    }
147
148
    /**
149
     * Buffers bytes into $this->buffer, removing uuencoding headers and footers
150
     * and decoding them.
151
     */
152 10
    private function fillBuffer(int $length) : void
153
    {
154
        // 5040 = 63 * 80, seems to be good balance for buffering in benchmarks
155
        // testing with a simple 'if ($length < x)' and calculating a better
156
        // size reduces speeds by up to 4x
157 10
        while ($this->buffer->getSize() < $length) {
158 10
            $read = $this->readToEndOfLine(5040);
159 10
            if ($read === '') {
160 9
                break;
161
            }
162 10
            $this->buffer->write($this->filterAndDecode($read));
163
        }
164
    }
165
166
    /**
167
     * Returns true if the end of stream has been reached.
168
     */
169 10
    public function eof() : bool
170
    {
171 10
        return ($this->buffer->eof() && $this->stream->eof());
172
    }
173
174
    /**
175
     * Attempts to read $length bytes after decoding them, and returns them.
176
     *
177
     * @param int $length
178
     */
179 10
    public function read($length) : string
180
    {
181
        // let Guzzle decide what to do.
182 10
        if ($length <= 0 || $this->eof()) {
183
            return $this->stream->read($length);
184
        }
185 10
        $this->fillBuffer($length);
186 10
        $read = $this->buffer->read($length);
187 10
        $this->position += \strlen($read);
188 10
        return $read;
189
    }
190
191
    /**
192
     * Writes the 'begin' UU header line.
193
     */
194 2
    private function writeUUHeader() : void
195
    {
196 2
        $filename = (empty($this->filename)) ? 'null' : $this->filename;
197 2
        $this->stream->write("begin 666 $filename");
198
    }
199
200
    /**
201
     * Writes the '`' and 'end' UU footer lines.
202
     */
203 2
    private function writeUUFooter() : void
204
    {
205 2
        $this->stream->write("\r\n`\r\nend\r\n");
206
    }
207
208
    /**
209
     * Writes the passed bytes to the underlying stream after encoding them.
210
     */
211 2
    private function writeEncoded(string $bytes) : void
212
    {
213 2
        $encoded = \preg_replace('/\r\n|\r|\n/', "\r\n", \rtrim(\convert_uuencode($bytes)));
214
        // removes ending '`' line
215 2
        $this->stream->write("\r\n" . \rtrim(\substr($encoded, 0, -1)));
216
    }
217
218
    /**
219
     * Prepends any existing remainder to the passed string, then checks if the
220
     * string fits into a uuencoded line, and removes and keeps any remainder
221
     * from the string to write.  Full lines ready for writing are returned.
222
     */
223 2
    private function handleRemainder(string $string) : string
224
    {
225 2
        $write = $this->remainder . $string;
226 2
        $nRem = \strlen($write) % 45;
227 2
        $this->remainder = '';
228 2
        if ($nRem !== 0) {
229 2
            $this->remainder = \substr($write, -$nRem);
230 2
            $write = \substr($write, 0, -$nRem);
231
        }
232 2
        return $write;
233
    }
234
235
    /**
236
     * Writes the passed string to the underlying stream after encoding it.
237
     *
238
     * Note that reading and writing to the same stream without rewinding is not
239
     * supported.
240
     *
241
     * Also note that some bytes may not be written until close or detach are
242
     * called.  This happens if written data doesn't align to a complete
243
     * uuencoded 'line' of 45 bytes.  In addition, the UU footer is only written
244
     * when closing or detaching as well.
245
     *
246
     * @param string $string
247
     * @return int the number of bytes written
248
     */
249 2
    public function write($string) : int
250
    {
251 2
        $this->isWriting = true;
252 2
        if ($this->position === 0) {
253 2
            $this->writeUUHeader();
254
        }
255 2
        $write = $this->handleRemainder($string);
256 2
        if ($write !== '') {
257 2
            $this->writeEncoded($write);
258
        }
259 2
        $written = \strlen($string);
260 2
        $this->position += $written;
261 2
        return $written;
262
    }
263
264
    /**
265
     * Returns the filename set in the UUEncoded header (or null)
266
     */
267
    public function getFilename() : string
268
    {
269
        return $this->filename;
270
    }
271
272
    /**
273
     * Sets the UUEncoded header file name written in the 'begin' header line.
274
     */
275
    public function setFilename(string $filename) : void
276
    {
277
        $this->filename = $filename;
278
    }
279
280
    /**
281
     * Writes out any remaining bytes and the UU footer.
282
     */
283 2
    private function beforeClose() : void
284
    {
285 2
        if (!$this->isWriting) {
286 1
            return;
287
        }
288 2
        if ($this->remainder !== '') {
289 2
            $this->writeEncoded($this->remainder);
290
        }
291 2
        $this->remainder = '';
292 2
        $this->isWriting = false;
293 2
        $this->writeUUFooter();
294
    }
295
296
    /**
297
     * @inheritDoc
298
     */
299 2
    public function close() : void
300
    {
301 2
        $this->beforeClose();
302 2
        $this->stream->close();
303
    }
304
305
    /**
306
     * @inheritDoc
307
     */
308
    public function detach()
309
    {
310
        $this->beforeClose();
311
        $this->stream->detach();
312
313
        return null;
314
    }
315
}
316