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

src/QuotedPrintableStream.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\StreamDecoratorTrait;
11
use Psr\Http\Message\StreamInterface;
12
use RuntimeException;
13
14
/**
15
 * GuzzleHttp\Psr7 stream decoder decorator for quoted printable streams.
16
 *
17
 * @author Zaahid Bateson
18
 */
19
class QuotedPrintableStream implements StreamInterface
20
{
21
    use StreamDecoratorTrait;
22
23
    /**
24
     * @var int current read/write position
25
     */
26
    private $position = 0;
27
28
    /**
29
     * @var string Last line of written text (used to maintain good line-breaks)
30
     */
31
    private $lastLine = '';
32
33
    /**
34
     * @var StreamInterface $stream
35
     * @phpstan-ignore-next-line
36
     */
37
    private $stream;
38
39
    /**
40
     * Overridden to return the position in the target encoding.
41
     */
42 3
    public function tell() : int
43
    {
44 3
        return $this->position;
45
    }
46
47
    /**
48
     * Returns null, getSize isn't supported
49
     *
50
     * @return null
51
     */
52 1
    public function getSize() : ?int
53
    {
54 1
        return null;
55
    }
56
57
    /**
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...
58
     * Not supported.
59
     *
60
     * @throws RuntimeException
61
     */
62 1
    public function seek(int $offset, int $whence = SEEK_SET) : void
63
    {
64 1
        throw new RuntimeException('Cannot seek a QuotedPrintableStream');
65
    }
66
67
    /**
68
     * Overridden to return false
69
     */
70 1
    public function isSeekable() : bool
71
    {
72 1
        return false;
73
    }
74
75
    /**
76
     * Reads $length chars from the underlying stream, prepending the past $pre
77
     * to it first.
78
     *
79
     * If the characters read (including the prepended $pre) contain invalid
80
     * quoted-printable characters, the underlying stream is rewound by the
81
     * total number of characters ($length + strlen($pre)).
82
     *
83
     * The quoted-printable encoded characters are returned.  If the characters
84
     * read are invalid, '3D' is returned indicating an '=' character.
85
     */
86 5
    private function readEncodedChars(int $length, string $pre = '') : string
87
    {
88 5
        $str = $pre . $this->stream->read($length);
89 5
        $len = \strlen($str);
90 5
        if ($len > 0 && !\preg_match('/^[0-9a-f]{2}$|^[\r\n]{1,2}.?$/is', $str) && $this->stream->isSeekable()) {
91 1
            $this->stream->seek(-$len, SEEK_CUR);
92 1
            return '3D';    // '=' character
93
        }
94 5
        return $str;
95
    }
96
97
    /**
98
     * Decodes the passed $block of text.
99
     *
100
     * If the last or before last character is an '=' char, indicating the
101
     * beginning of a quoted-printable encoded char, 1 or 2 additional bytes are
102
     * read from the underlying stream respectively.
103
     *
104
     * @return string The decoded string
105
     */
106 8
    private function decodeBlock(string $block) : string
107
    {
108 8
        if (\substr($block, -1) === '=') {
109 5
            $block .= $this->readEncodedChars(2);
110 8
        } elseif (\substr($block, -2, 1) === '=') {
111 4
            $first = \substr($block, -1);
112 4
            $block = \substr($block, 0, -1);
113 4
            $block .= $this->readEncodedChars(1, $first);
114
        }
115 8
        return \quoted_printable_decode($block);
116
    }
117
118
    /**
119
     * Reads up to $length characters, appends them to the passed $str string,
120
     * and returns the total number of characters read.
121
     *
122
     * -1 is returned if there are no more bytes to read.
123
     */
124 8
    private function readRawDecodeAndAppend(int $length, string &$str) : int
125
    {
126 8
        $block = $this->stream->read($length);
127 8
        if ($block === '') {
128 8
            return -1;
129
        }
130 8
        $decoded = $this->decodeBlock($block);
131 8
        $count = \strlen($decoded);
132 8
        $str .= $decoded;
133 8
        return $count;
134
    }
135
136
    /**
0 ignored issues
show
Parameter $length should have a doc-comment as per coding-style.
Loading history...
137
     * Reads up to $length decoded bytes from the underlying quoted-printable
138
     * encoded stream and returns them.
139
     */
140 8
    public function read($length) : string
141
    {
142
        // let Guzzle decide what to do.
143 8
        if ($length <= 0 || $this->eof()) {
144
            return $this->stream->read($length);
145
        }
146 8
        $count = 0;
147 8
        $bytes = '';
148 8
        while ($count < $length) {
149 8
            $nRead = $this->readRawDecodeAndAppend($length - $count, $bytes);
150 8
            if ($nRead === -1) {
151 8
                break;
152
            }
153 8
            $this->position += $nRead;
154 8
            $count += $nRead;
155
        }
156 8
        return $bytes;
157
    }
158
159
    /**
160
     * Writes the passed string to the underlying stream after encoding it as
161
     * quoted-printable.
162
     *
163
     * Note that reading and writing to the same stream without rewinding is not
164
     * supported.
165
     *
166
     * @param string $string
167
     *
168
     * @return int the number of bytes written
169
     */
170 1
    public function write($string) : int
171
    {
172 1
        $encodedLine = \quoted_printable_encode($this->lastLine);
173 1
        $lineAndString = \rtrim(\quoted_printable_encode($this->lastLine . $string), "\r\n");
174 1
        $write = \substr($lineAndString, \strlen($encodedLine));
175 1
        $this->stream->write($write);
176 1
        $written = \strlen($string);
177 1
        $this->position += $written;
178
179 1
        $lpos = \strrpos($lineAndString, "\n");
180 1
        $lastLine = $lineAndString;
181 1
        if ($lpos !== false) {
182 1
            $lastLine = \substr($lineAndString, $lpos + 1);
183
        }
184 1
        $this->lastLine = \quoted_printable_decode($lastLine);
185 1
        return $written;
186
    }
187
188
    /**
189
     * Writes out a final CRLF if the current line isn't empty.
190
     */
191 1
    private function beforeClose() : void
192
    {
193 1
        if ($this->isWritable() && $this->lastLine !== '') {
194 1
            $this->stream->write("\r\n");
195 1
            $this->lastLine = '';
196
        }
197 1
    }
198
199
    /**
200
     * @inheritDoc
201
     */
202 1
    public function close() : void
203
    {
204 1
        $this->beforeClose();
205 1
        $this->stream->close();
206 1
    }
207
208
    /**
209
     * @inheritDoc
210
     */
211
    public function detach()
212
    {
213
        $this->beforeClose();
214
        $this->stream->detach();
215
216
        return null;
217
    }
218
}
219