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
Coding Style
introduced
by
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
|
|||
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 |