Test Failed
Push — master ( 712b9e...56837b )
by Zaahid
04:49
created

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