CharsetStream::seek()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the ZBateson\StreamDecorator 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
use ZBateson\MbWrapper\MbWrapper;
14
15
/**
16
 * GuzzleHttp\Psr7 stream decoder extension for charset conversion.
17
 *
18
 * @author Zaahid Bateson
19
 */
20
class CharsetStream implements StreamInterface
21
{
22
    use StreamDecoratorTrait;
23
24
    /**
25
     * @var MbWrapper the charset converter
26
     */
27
    protected MbWrapper $converter;
28
29
    /**
30
     * @var string charset of the source stream
31
     */
32
    protected string $streamCharset = 'ISO-8859-1';
33
34
    /**
35
     * @var string charset of strings passed in write operations, and returned
36
     *      in read operations.
37
     */
38
    protected string $stringCharset = 'UTF-8';
39
40
    /**
41
     * @var int current read/write position
42
     */
43
    private int $position = 0;
44
45
    /**
46
     * @var int number of $stringCharset characters in $buffer
47
     */
48
    private int $bufferLength = 0;
49
50
    /**
51
     * @var string a buffer of characters read in the original $streamCharset
52
     *      encoding
53
     */
54
    private string $buffer = '';
55
56
    /**
57
     * @var StreamInterface $stream
58
     */
59
    private StreamInterface $stream;
60
61
    /**
62
     * @param StreamInterface $stream Stream to decorate
63
     * @param string $streamCharset The underlying stream's charset
64
     * @param string $stringCharset The charset to encode strings to (or
65
     *        expected for write)
66
     */
67 9
    public function __construct(StreamInterface $stream, string $streamCharset = 'ISO-8859-1', string $stringCharset = 'UTF-8')
68
    {
69 9
        $this->stream = $stream;
70 9
        $this->converter = new MbWrapper();
71 9
        $this->streamCharset = $streamCharset;
72 9
        $this->stringCharset = $stringCharset;
73
    }
74
75
    /**
76
     * Overridden to return the position in the target encoding.
77
     */
78 2
    public function tell() : int
79
    {
80 2
        return $this->position;
81
    }
82
83
    /**
84
     * Returns null, getSize isn't supported
85
     *
86
     * @return null
87
     */
88 1
    public function getSize() : ?int
89
    {
90 1
        return null;
91
    }
92
93
    /**
94
     * Not supported.
95
     *
96
     * @param int $offset
97
     * @param int $whence
98
     * @throws RuntimeException
99
     */
100 1
    public function seek($offset, $whence = SEEK_SET) : void
101
    {
102 1
        throw new RuntimeException('Cannot seek a CharsetStream');
103
    }
104
105
    /**
106
     * Overridden to return false
107
     */
108 1
    public function isSeekable() : bool
109
    {
110 1
        return false;
111
    }
112
113
    /**
114
     * Reads a minimum of $length characters from the underlying stream in its
115
     * encoding into $this->buffer.
116
     *
117
     * Aligning to 4 bytes seemed to solve an issue reading from UTF-16LE
118
     * streams and pass testReadUtf16LeToEof, although the buffered string
119
     * should've solved that on its own.
120
     */
121 7
    private function readRawCharsIntoBuffer(int $length) : void
122
    {
123 7
        $n = (int) \ceil(($length + 32) / 4.0) * 4;
124 7
        while ($this->bufferLength < $n) {
125 7
            $raw = $this->stream->read($n + 512);
126 7
            if ($raw === '') {
127 7
                return;
128
            }
129 7
            $this->buffer .= $raw;
130 7
            $this->bufferLength = $this->converter->getLength($this->buffer, $this->streamCharset);
131
        }
132
    }
133
134
    /**
135
     * Returns true if the end of stream has been reached.
136
     */
137 7
    public function eof() : bool
138
    {
139 7
        return ($this->bufferLength === 0 && $this->stream->eof());
140
    }
141
142
    /**
143
     * Reads up to $length decoded chars from the underlying stream and returns
144
     * them after converting to the target string charset.
145
     *
146
     * @param int $length
147
     */
148 7
    public function read($length) : string
149
    {
150
        // let Guzzle decide what to do.
151 7
        if ($length <= 0 || $this->eof()) {
152 1
            return $this->stream->read($length);
153
        }
154 7
        $this->readRawCharsIntoBuffer($length);
155 7
        $numChars = \min([$this->bufferLength, $length]);
156 7
        $chars = $this->converter->getSubstr($this->buffer, $this->streamCharset, 0, $numChars);
157
158 7
        $this->position += $numChars;
159 7
        $this->buffer = $this->converter->getSubstr($this->buffer, $this->streamCharset, $numChars);
160 7
        $this->bufferLength -= $numChars;
161
162 7
        return $this->converter->convert($chars, $this->streamCharset, $this->stringCharset);
163
    }
164
165
    /**
166
     * Writes the passed string to the underlying stream after converting it to
167
     * the target stream encoding.
168
     *
169
     * @param string $string
170
     * @return int the number of bytes written
171
     */
172 1
    public function write($string) : int
173
    {
174 1
        $converted = $this->converter->convert($string, $this->stringCharset, $this->streamCharset);
175 1
        $written = $this->converter->getLength($converted, $this->streamCharset);
176 1
        $this->position += $written;
177 1
        return $this->stream->write($converted);
178
    }
179
}
180