QuotedPrintableStream::detach()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
ccs 0
cts 4
cp 0
crap 2
rs 10
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 int $position = 0;
27
28
    /**
29
     * @var string Last line of written text (used to maintain good line-breaks)
30
     */
31
    private string $lastLine = '';
32
33
    /**
34
     * @var StreamInterface $stream
35
     * @phpstan-ignore-next-line
36
     */
37
    private StreamInterface $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
    /**
58
     * Not supported.
59
     *
60
     * @param int $offset
61
     * @param int $whence
62
     * @throws RuntimeException
63
     */
64 1
    public function seek($offset, $whence = SEEK_SET) : void
65
    {
66 1
        throw new RuntimeException('Cannot seek a QuotedPrintableStream');
67
    }
68
69
    /**
70
     * Overridden to return false
71
     */
72 1
    public function isSeekable() : bool
73
    {
74 1
        return false;
75
    }
76
77
    /**
78
     * Reads $length chars from the underlying stream, prepending the past $pre
79
     * to it first.
80
     *
81
     * If the characters read (including the prepended $pre) contain invalid
82
     * quoted-printable characters, the underlying stream is rewound by the
83
     * total number of characters ($length + strlen($pre)).
84
     *
85
     * The quoted-printable encoded characters are returned.  If the characters
86
     * read are invalid, '3D' is returned indicating an '=' character.
87
     */
88 5
    private function readEncodedChars(int $length, string $pre = '') : string
89
    {
90 5
        $str = $pre . $this->stream->read($length);
91 5
        $len = \strlen($str);
92 5
        if ($len > 0 && !\preg_match('/^[0-9a-f]{2}$|^[\r\n]{1,2}.?$/is', $str) && $this->stream->isSeekable()) {
93 1
            $this->stream->seek(-$len, SEEK_CUR);
94 1
            return '3D';    // '=' character
95
        }
96 5
        return $str;
97
    }
98
99
    /**
100
     * Decodes the passed $block of text.
101
     *
102
     * If the last or before last character is an '=' char, indicating the
103
     * beginning of a quoted-printable encoded char, 1 or 2 additional bytes are
104
     * read from the underlying stream respectively.
105
     *
106
     * @return string The decoded string
107
     */
108 8
    private function decodeBlock(string $block) : string
109
    {
110 8
        if (\substr($block, -1) === '=') {
111 5
            $block .= $this->readEncodedChars(2);
112 8
        } elseif (\substr($block, -2, 1) === '=') {
113 4
            $first = \substr($block, -1);
114 4
            $block = \substr($block, 0, -1);
115 4
            $block .= $this->readEncodedChars(1, $first);
116
        }
117 8
        return \quoted_printable_decode($block);
118
    }
119
120
    /**
121
     * Reads up to $length characters, appends them to the passed $str string,
122
     * and returns the total number of characters read.
123
     *
124
     * -1 is returned if there are no more bytes to read.
125
     */
126 8
    private function readRawDecodeAndAppend(int $length, string &$str) : int
127
    {
128 8
        $block = $this->stream->read($length);
129 8
        if ($block === '') {
130 8
            return -1;
131
        }
132 8
        $decoded = $this->decodeBlock($block);
133 8
        $count = \strlen($decoded);
134 8
        $str .= $decoded;
135 8
        return $count;
136
    }
137
138
    /**
139
     * Reads up to $length decoded bytes from the underlying quoted-printable
140
     * encoded stream and returns them.
141
     *
142
     * @param int $length
143
     */
144 8
    public function read($length) : string
145
    {
146
        // let Guzzle decide what to do.
147 8
        if ($length <= 0 || $this->eof()) {
148
            return $this->stream->read($length);
149
        }
150 8
        $count = 0;
151 8
        $bytes = '';
152 8
        while ($count < $length) {
153 8
            $nRead = $this->readRawDecodeAndAppend($length - $count, $bytes);
154 8
            if ($nRead === -1) {
155 8
                break;
156
            }
157 8
            $this->position += $nRead;
158 8
            $count += $nRead;
159
        }
160 8
        return $bytes;
161
    }
162
163
    /**
164
     * Writes the passed string to the underlying stream after encoding it as
165
     * quoted-printable.
166
     *
167
     * Note that reading and writing to the same stream without rewinding is not
168
     * supported.
169
     *
170
     * @param string $string
171
     *
172
     * @return int the number of bytes written
173
     */
174 1
    public function write($string) : int
175
    {
176 1
        $encodedLine = \quoted_printable_encode($this->lastLine);
177 1
        $lineAndString = \rtrim(\quoted_printable_encode($this->lastLine . $string), "\r\n");
178 1
        $write = \substr($lineAndString, \strlen($encodedLine));
179 1
        $this->stream->write($write);
180 1
        $written = \strlen($string);
181 1
        $this->position += $written;
182
183 1
        $lpos = \strrpos($lineAndString, "\n");
184 1
        $lastLine = $lineAndString;
185 1
        if ($lpos !== false) {
186 1
            $lastLine = \substr($lineAndString, $lpos + 1);
187
        }
188 1
        $this->lastLine = \quoted_printable_decode($lastLine);
189 1
        return $written;
190
    }
191
192
    /**
193
     * Writes out a final CRLF if the current line isn't empty.
194
     */
195 1
    private function beforeClose() : void
196
    {
197 1
        if ($this->isWritable() && $this->lastLine !== '') {
198 1
            $this->stream->write("\r\n");
199 1
            $this->lastLine = '';
200
        }
201
    }
202
203
    /**
204
     * @inheritDoc
205
     */
206 1
    public function close() : void
207
    {
208 1
        $this->beforeClose();
209 1
        $this->stream->close();
210
    }
211
212
    /**
213
     * @inheritDoc
214
     */
215
    public function detach()
216
    {
217
        $this->beforeClose();
218
        $this->stream->detach();
219
220
        return null;
221
    }
222
}
223